Konsist is more than you might think

Kacper Wojciechowski
6 min readFeb 2, 2024
Photo by Alexander Hafemann on Unsplash

Konsist is a library that every Medium reader has heard about recently. It was in development for about a year (based on commits), but somehow has blown up recently. There were many articles that swarm my feed. At first glance, most people focus on one feature — it can verify your code architecture and help you with refactors. After reading some of those articles I decided to have a deep dive myself, and quickly realized that the main selling point from those articles is just the tip of the iceberg of how powerful this tool is.

Konsist is the ultimate tool for enforcing code conventions and consistency contracts between the developers!

What it is and how to use it?

Konsist is a tool that reads all definitions from Kotlin files in the project. Classes, interfaces, functions, properties, imports, packages — all you can ever imagine. Then gives you APIs to verify those definitions in the form of a Unit Test. To use Konsist all you need is to add this library to your tests:

testImplementation("com.lemonappdev:konsist:0.13.0")

Assuming you already have Unit Tests in your project (remember to write Unit Tests to your code people!) then this is all you need to use Konsist. It is compatible with JUnit and Kotest. In this article, I will use JUnit 4, but it is also compatible with JUnit 5 dynamic tests which looks like a go-to approach for readable test results.

Test project overview

Konsist is a tool that will work in any project using Kotlin. However, I’m an Android Developer so, of course, the example will be an Android project. It is a super simple example of an MVVM application using Compose, Clean Architecture, Room, and Dagger Hilt. It has a single screen named “Home” that queries the database using coroutines and then displays some text. For example simplicity, it is placed in a single module, but Konsist works fully multimodule.

Konsist usage

Let’s start with a great feature, assuring that there are no architectural layers violations:

In this code snippet, we define the 3 architectural layers that are present in my example application. Those definitions are package-based so those checks are not dependent on modules. Then we define dependencies of those layers, so:
- data layer should depend on nothing
- domain layer depends on the data layer
- UI layer depends on the domain layer

For the purpose of this article let’s imagine a team member that tries to make shortcuts in development and always forgets about coding principles that all other team members agreed on. Let’s call him John.

This check iterates through the files and verifies if there are no layer dependencies violations. John does some shortcuts and tries to query the value from the database directly in the ViewModel using the Dao interface from the database package. Normally, you will leave a Code Review comment that this is not the way to go. Using Konsist you won’t have to, because his Unit Tests will get red!

That’s amazing! Now let's dig deeper into the possibilities.

Function assertArchitecture { … } is just a ready-to-use check that you would probably be able to write yourself using the killer feature — the granular API that has access to every aspect of Kotlin code you can imagine.

With this tool, you will be able to write all of the needed checks for code consistency and code conventions. Let’s start with an example:

Let’s break down what happens in this example:
1. Fetch all classes in the project
2. Filter classes with the name ending with the keyword “UseCase”
3. Assert that every class resides in package “com.iteo.konsisttest.domain”

As you probably understood already — this check verifies that UseCases are placed in the domain layer. Our dear John just finished the work on a new feature but it seems like he has made a mistake and placed the UseCase in the wrong package. This is especially hard to catch in Code Review as nobody ever checks the package. Usually, it gets merged and you realize that something is not right when it is too late. With this check, this won’t happen as he won’t be able to merge the change, because Unit Tests will fail on the pipeline!

This is just a very simple example. For this case, you would probably also want to check if they are placed in the correct module if it exposes a single function and it is an invoke function (or whatever is your code convention), and if this function has an “operator” modifier. The possibilities for validation are enormous.

To show you the power of Konsist let’s have a look at a much more complicated example:

This check:
1. Fetches all classes
2. Filters all classes of type ViewModel
3. Check if the class has only one constructor
4. Check if the class constructor parameters have either no modifiers or val and private modifiers.

Are you triggered by constantly leaving Code Review comments to John to stop using public values as injected constructor parameters? Say no more! With Konsist it will never happen again!

This is brilliant! We can go much much deeper with that. You can check the annotations of classes. Check if classes have the correct naming convention (f.e. verify that ViewModel has the “ViewModel” name suffix). If ViewModel does not expose some unnecessary public properties or functions. If Compose preview functions have a private modifier. If all files placed in the “extensions” package have the “Extensions” name suffix. If all UseCase’s have a test class.

The possibilities are enormous. Konsist documentation has a great set of examples that will inspire you with what you wish to achieve.

Conclusion

Now let’s recall all of the memories where you decide on some solution with all the teammates. How do you track if this rule was applied and you won’t forget about this decision? Sometimes there are ideas — let’s document them in the Confluence. But documentation has nothing to do with enforcing the codebase style and convention. It will never make sure that developers follow the rules that are written on those pages.

The best way to enforce that the rules are followed is to not allow the malicious code to be merged. This seems a bit harsh but everyone makes some mistakes and sometimes those mistakes are overseen during the Code Review. People make mistakes. Life is life. But well-written tests will never make mistakes.

This library is this Confluence page with code conventions and rules that everyone, sooner or later, forgets about. But it gives you tools to actually enforce those rules in a strict way. You won’t forget about them if your pipeline will keep failing!

--

--