Blog Infos
Author
Published
Topics
Published
Topics

taken from https://docs.konsist.lemonappdev.com/

 

In my last article, I wrote about my first experience with Akkurate, a new validation library for Kotlin. But that’s not the only new library that was introduced recently which has aroused my interest. Therefore it’s time to give insights into the first experience with another newcomer – Konsist.

So what is Konsist?

The official documentation gives a good and concise explanation of its purpose and I can not sum it up better.

Konsist is a static code analyzer for the Kotlin language. It is compatible 
with various Kotlin projects including Android projects, Spring projects, 
and Kotlin Multiplatform projects.
Konsist simplifies the process of maintaining codebase consistency by 
upholding coding conventions and safeguarding the project's architecture. 
It empowers developers to create consistency checks through unit tests, which
can be run during pull request (PR) reviews for verification.
(taken from https://docs.konsist.lemonappdev.com/getting-started/readme)

It is important to know that Konsist is currently in a 0.x version available. The project is released so that the Kotlin community can use it and give valuable feedback to bring further development in the correct direction. Normally using a 0.x version in a productive project is a great risk because, in such an early stage of a library, a lot in the public API can change. This can lead to high maintenance costs and developers may not be enthusiastic about it. That is true for code that affects the production use of the application (can block running the application and shipping of new releases) but in the case of Konsist this is not that critical because all the rules that are written for analyzing the code are test-scoped. That means in the worst case I just can disable them and the production code is not affected. That’s the reason I’m not waiting until the first stable 1.x version but giving it a try now.

Before I give an overview of how Konsist works and what is possible with it I will talk about, what is my personal use-case. There are a lot of linter available in the Kotlin ecosystem and they have different purposes. In my application development, there were so far three tools used:

  • ktlint for having a unified code format across the code base.
  • detekt for writing code according to the best practices developed by the Kotlin community.
  • ArchUnit for enforcing the rules for the application architecture.

For all of them, I already wrote an article on my blog. But it is just the last one that does not fully support my requirements. ArchUnit is a framework that is written for Java and because of the compatibility of Kotlin with Java code, it also can be used in pure Kotlin applications with the JVM target. There is just a limitation that the Kotlin-specific language features like extension functions or top-level functions are not supported.

So my hope is that Konsist, because of its Kotlin native implementation can help me to remove these limits. Now enough words, let’s have a look at how my existing requirements (implemented by ArchUnit tests) can be solved and also what additional things are possible.

Configuration

Konsist can be added to your application by adding it as dependency to the build.gradle.kts file.

dependencies{
  testImplementation("com.lemonappdev:konsist:0.12.2")
}

As already mentioned above it is only test-scoped available in the application so the productive code is not affected and there is nothing part of the final artifact of the application.

The initial step is to specify all files/classes that should be scanned for verifying against the requirements. Already this step is showing a great difference between ArchUnit and Konsist. While the first one is working on the result of the compilation (the content of the build directory), the second one is scanning the source code (the “Kotlin” files).

Both provide the possibility to limit the scope of the analysis to either only a single module (in multi-module projects), a special sourceset, only production code, or even a single file. Below you can see some examples:

Konsist.scopeFromProduction(moduleName = "MyModule", )

Konsist.scopeFromPackage(packagee = "com.poisonedyouth.exmple", moduleName = "MyModule")

Konsist.scopeFromFile(path = "src/main/kotlin/com/poisonedyouth/example/MyFile.kt")

The next step is to select what declarations I want to analyze. Let me show that in an example. I use multiple declarations in a single file called Test.kt with the below content:

import org.springframework.stereotype.Controller

class TestClass{
    
}

class OtherTestClass{

}

interface TestInterface{
    
}

@Controller
annotation class TestAnnotation{
    
}

Depending on the declaration selector I get a different result.

// This prints "Test.kt" 
Konsist.scopeFromFile("src/main/kotlin/com/poisonedyouth/example/Test.kt")
   .files
   .print()

// This prints "org.springframework.stereotype.Controller"
Konsist.scopeFromFile("accounting/src/main/kotlin/com/sevdesk/accounting/booking/taxinformation/api/impl/Test.kt")
   .imports
   .print()

// This prints "TestInterface"
Konsist.scopeFromFile("accounting/src/main/kotlin/com/sevdesk/accounting/booking/taxinformation/api/impl/Test.kt")
   .interfaces
   .print()

// This prints "TestClass, OtherTestClass, TestAnnotation"
Konsist.scopeFromFile("accounting/src/main/kotlin/com/sevdesk/accounting/booking/taxinformation/api/impl/Test.kt")
   .classes
   .print()

With this, depending on the requirement I can limit the elements that are processed to the ones I need. The above examples are just a small part of what is possible. You can have a look at the documentation about what is possible.

As soon as the declarations to process are filtered I can start by asserting for the expected conditions.

In the following part, I will show different real examples I’m currently using in my applications, which should show what is possible with Konsist (even in this early stage).

Runtime Optimization

Most of my tests that are part of a single test class are using the same scope as input, so it makes sense to move the configuration to the companion object of the test class.

companion object {
     val projectScope = Konsist.scopeFromProduction(moduleName = module)
}

With this, every test can use the projectScope property. This improves the runtime of executing all the tests in one class dramatically (10 test methods – without static configuration ~5 seconds, with static configuration < 1 second).

Imports

The first use case that I have in all my applications is to check files for imports. Either an expected one should be available or a specified one should not be available. In e

OUR VIDEO RECOMMENDATION

Jobs

No results found.

// The specified imports are available
Konsist.scopeFromProduction()
   .files
   .withPackage("com.poisonedyouth.example.port..")
   .assert {
       it.hasImportWithName(
          "com.poisonedyouth.example.domain.."
       )
    }

// The specified imports are not available
Konsist.scopeFromProduction()
   .files
   .withPackage("com.poisonedyouth.example.common..")
   .assertNot {
       it.hasImportWithName(
          "com.poisonedyouth.example.domain.."
          "org.springframework...",
       )
    }

There is just one limitation with the given built-in hasImportWithName – function, if the file has no imports at all, it will fail. There are some use cases where this is leading to unwanted issues. But that’s no problem, I just can write my own extension function that is working as expected.

private fun KoFileDeclaration.hasImportWithNameOrNone(vararg names: String): Boolean {
    if (this.imports.isEmpty()) {
        return true
    }
    return imports.all { import ->
        names.any { name ->
            LocationUtil.resideInLocation(name, import.name)
        }
    }
}

This is just one example for how easy the built-in functionality can be extended in case they don’t cover all the personal needs. Because the project is in an early stage of development, the probability that missing built-in functionality is added in one of the next releases is relatively high. Just contact the maintainers in either the official slack channel (#konsist as part of kotlinlang.slack.com) or directly in the github repository.

Data Class

Data classes are designed to be immutable. All primary constructor parameters should use val instead of var, what makes it easier to deal with the state, especially in multithreaded environments. Instead of changing the internal state of an instance from outside the copy – constructor should be used to create a new instance. To check that there is no var used, the below declaration can be used:

Konsist.scopeFromProduction()
   .classes()
   .withModifier(KoModifier.DATA)
   .properties(includeNested = true, includeLocal = true)
   .assert{
      it.hasValModifier
   }

The withModifier() – function can be used to limit the classes that are included in the assertion block. There are a lot of options possible. Another use-case for data classes is to check that there is no init – block used. As long as try-catch-throw is used for handling exception the init – block is the main place for validation of the input, to only allow valid domain models. Because meanwhile Either is used for exception handling in most of my projects, and this is no longer possible. The init – block cannot return an Either type. So to not by mistake forget to remove this blocks, I can write the following check:

Konsist.scopeFromProduction()
   .classes(includeNested = true)
   .withModifier(KoModifier.DATA)
   .assert {
      it.initBlocks.isEmpty()
   }

Data classes automatically implement the equals() and hashCode() function including the primary constructor parameters. But it is not always possible to use data classes to model domain models. Especially when dealing with Either exception handling, it can be necessary to use a class with a private constructor and use a factory method for creation of objects. As soon as a normal class is used, which should be comparable later or is put to collections (especially sets) it is necessary to implement equals() and hashCode() manually (see https://kt.academy/article/ek-equals for details about why this is important).

To be sure that always both methods are implemented I can use the below check that shows violations for all classes that only implement one of the both methods.

Konsist.scopeFromProduction()
  .classes(includeNested = true)
  .assertNot {
      it.countFunctions { functionDeclaration ->
          functionDeclaration.name in listOf("equals", "hashCode")
      } == 1
  }
Annotations

Another category of checks that can be implemented are related to annotations. Especially when working in the Spring / Spring Boot ecosystem annotations play an important role and occur all over the project.

It is easy to enforce the naming of Spring components. If it is intended that all rest controller components (marked by the annotation) also contain a “Controller” suffix, this can be achieved by below snippet:

Konsist.scopeFromProduction()
  .classes(includeNested = true)
  .withAnnotationOf(RestController::class)
  .assert {
      it.name.endsWith("Controller")
  }

The same can also be done for the @Service@Repository or other used annotations.

Even though I use SpringBoot as framework I still want the domain to be free of framework-specific code. So I want to check that inside the domain package there are no Spring specific annotations used.

Konsist.scopeFromProduction()
    .classes(includeNested = true)
    .withPackage("com.poisonedyouth.example.domain..")
    .assertNot { 
        it.hasAnnotationOf(
           Configuration::class, 
           Service::class, 
           Repository::class
        )
     }

When using JPA it also can be a use-case to prevent using the domain model as an entity for persistence. So the above check can be extended by the @Entity annotation class. I primarily use Exposed for persistence so this is no problem in my projects.

A more complex use-case related to annotations, is to check that all Spring components that rely on dependencies, that are injected by construcor, are using the abstraction and not the concrete implementation. This is necessary to be able to replace the concrete implementation without changing the usages (either for testing or for a special production environment).

This is a workaround because Konsist currently not supports to check the constructor parameter for an interface or abstract type.

val interfaces = projectScope
    .interfaces()
    .map { it.name }
val abstractClasses = projectScope
    .classes()
    .withModifier(KoModifier.ABSTRACT)
    .map{ it.name}
val expectedComponents = interfaces + abstractClasses


Konsist.scopeFromProduction()
     .classes()
     .withAnnotationOf(
         RestController::class,
         Service::class,
         Repository::class,
       )
      .assert {
           it.primaryConstructor!!.parameters.all { parameter ->
                    parameter.hasType { type ->
                        expectedComponents.any { name ->
                            type.sourceType == name
                        }
                    }
            }
      }

Maybe with one of the next releases a more elegant solution will be possible.

Visibility

When working with multi-module projects with Gradle it is important to define the dependencies between single modules. For having as much independence between modules as possible I can introduce internal APIs. As an example module A provides an API for providing user data that is necessary for module B. Instead of directly using the repository of module A (which couples both modules very tight), module A provides an API that allows to load the data using a contract. Because the repository is visible for module B there is the risk that it is still used beside the API. So to make only the API and its types visible, I can set all other classes to internal. To enforce this also for newly added classes I can add the below check:

Konsist.scopeFromProduction()
  .classes()
  .withPackage("com.poisonedyouth.example.impl..")
  .assert {
      it.modifiers.contains(KoModifier.INTERNAL)
  }
Architecture

Konsist also supports a special requirement for verifying the architecture of applications. The provided functionality can also be achieved by the already described functionality above but it is not such concise.

There is a special function called assertArchitecture(), inside of which I define the layers my application consists of. Each layer is identified by a root package. After the definition of the layers I can configure which access between the layers is accepted. With this test I verify that e.g. a domain model is not by mistake directly used in the adapter (because I want to use a mapping of types between all layers).

Konsist.scopeFromProduction()
  .assertArchitecture {
     val domain = Layer("domain", "com.poisonedyouth.example.domain..")
     val adapter = Layer("adapter", "com.poisonedyouth.example.adapter..")
     val port = Layer("port", "com.poisonedyouth.example.port..")
     val common = Layer("common", "com.poisonedyouth.example.common..")

     domain.dependsOn(common)
     adapter.dependsOn(common, port)
     port.dependsOn(domain, common)
     common.dependsOnNothing()
  }

This test can easily be changed to fit the concrete architecture that is used in the application under test (3 – layer architecture, clean architecture, …)

Summary

Today I summarized my first experience with using Konsist in my applications. My drive was to replace the existing ArchUnit tests and remove the limitations that exist, because Kotlin specific features are not natively supported. Also, I always try to use Kotlin native solutions because they mostly bring the general advantages of Kotlin compared to Java in terms of user experience.

It took me just a few hours to migrate all of the existing ArchUnit tests. The usage of Konsist is very intuitive and the documentation perfectly guides with examples. Meanwhile, I extended the original migrated tests by a lot of additional ones that are very easily written and there are nearly no limitations.

For me, there are several pros for using Konsist in my applications to extend other tools like detekt or ktlint that I already use as an integral part of all my applications:

  • Kotlin specific features are supported.
  • Very concise tests. Often there are just 10–20 lines of code for each.
  • Community-driven further development of Konsist. Feedback is directly recognized by the maintainers for new releases.
  • Problems or questions are very fast solved in the corresponding slack channel (#konsist)
  • Extensibility. It is easy to write custom conditions for checks that are currently not built-in.
  • The library is very well documented both in code and in the official documentation.

The above-described examples are just the current state of my usage and besides my daily work, there are always new use-cases that I find. Also Konsist is permanently further developed and new things are possible. So maybe there will be an additional article in the near future.

If you need additional explanations, you can look at the official documentation which is very well written and gives examples for all the available features.

There is even a special section for snippets, that can contain complete runnable tests for specific areas of usage like Android or Spring, but also general ones. These can be used for an easy start of writing custom requirements.

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu