Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
The following example provides the minimum setup for defining and running a single Konsist test.
At a high-level Konsist check is a Unit test following multiple implicit steps.
3 steps are required for a declaration check and 4 steps are required for an architecture check:
Konsist is a structural linter (static code analyzer) designed for Kotlin language. Verifying codebase with Konsist enables development teams to enforce architectural rules and class structures through automated testing.
Konsist offers comprehensive verification capabilities that enable developers to enforce architectural rules and maintain code consistency, thereby improving the readability and maintainability of the code. It's like ArchUnit, but for Kotlin language. Whether you're working on Android, Spring, or Kotlin Multiplatform projects, Konsist has got you covered.
The Konsist API provides developers with the capability to create custom checks through unit tests, customized to align with the project's unique requirements. Additionally, it offers smooth integration with leading testing frameworks, including JUnit4, JUnit5, and Kotest, further streamlining the development process.
Konsist offers two types of checks, namely and , to thoroughly evaluate the codebase.
The first type involves declaration checks, where custom tests are created to identify common issues and violations at the declaration level (classes, functions, properties, etc.). These cover various aspects such as class naming, package structure, visibility modifiers, presence of annotations, etc. Here are a few ideas of things to check:
Every child class extending ViewModel must have ViewModel suffix
Classes with the @Repository annotation should reside in ..repository.. package
Every class constructor has alphabetically ordered parameters
Here is a sample test that verifies if every use case class resides in domain.usecase package:
The second type of revolves around architecture boundaries - they are intended to maintain the separation of concerns between layers.
Consider this simple 3 layer of :
The domain layer is independent
The data layer depends on domain layer
The presentation layer depends on domain
Here is a Konsist test that verifies if Clean Architecture dependency requirements are valid:
By utilizing Konsist, teams can be confident that their Kotlin codebase remains standardized and aligned with best practices, making code reviews more efficient and code maintenance smoother.
Every constructor parameter has a name derived from the class name
Field injection and m prefix is forbidden
Every public member in api package must be documented with KDoc
and more...
etc.

Add the following dependency to the module\build.gradle.kts file:
dependencies {
testImplementation("com.lemonappdev:konsist:0.17.3")
}Add the following dependency to the module\build.gradle file:
dependencies {
testImplementation "com.lemonappdev:konsist:0.17.3"
}Add the following dependency to the module\pom.xml file:
<dependency>
<groupId>com.lemonappdev</groupId>
<artifactId>konsist</artifactId>
<version>0.17.3</version>
<scope>test</scope>
</dependency>class UseCaseKonsistTest {
@Test
fun `every use case reside in use case package`() {
Konsist
.scopeFromProject() // Define the scope containing all Kotlin files present in the project
.classes() // Get all class declarations
.withNameEndingWith("UseCase") // Filter classes heaving name ending with 'UseCase'
.assertTrue { it.resideInPackage("..domain.usecase..") } // Assert that each class resides in 'any domain.usecase any' package
}
}
class UseCaseKonsistTest : FreeSpec({
"every use case reside in use case package" {
Konsist
.scopeFromProject() // Define the scope containing all Kotlin files present in the project
.classes() // Get all class declarations
.withNameEndingWith("UseCase") // Filter classes heaving name ending with 'UseCase'
.assertTrue (
testName = this.testCase.name.testName
){
it.resideInPackage("..domain.usecase..")
} // Assert that each class resides in 'any domain.usecase any' package
}
})class ArchitectureTest {
@Test
fun `clean architecture layers have correct dependencies`() {
Konsist
.scopeFromProject() // Define the scope containing all Kotlin files present in project
.assertArchitecture { // Assert architecture
// Define layers
val domain = Layer("Domain", "com.myapp.domain..")
val presentation = Layer("Presentation", "com.myapp.presentation..")
val data = Layer("Data", "com.myapp.data..")
// Define architecture assertions
domain.dependsOnNothing()
presentation.dependsOn(domain)
data.dependsOn(domain)
}
}
}class ArchitectureTest : FreeSpec({
"every use case reside in use case package" {
Konsist
.scopeFromProject() // Define the scope containing all Kotlin files present in project
.assertArchitecture { // Assert architecture
// Define layers
val domain = Layer("Domain", "com.myapp.domain..")
val presentation = Layer("Presentation", "com.myapp.presentation..")
val data = Layer("Data", "com.myapp.data..")
// Define architecture assertions
domain.dependsOnNothing()
presentation.dependsOn(domain)
data.dependsOn(domain)
}
}
})repositories {
mavenCentral()
}The Konsist logo can be found in the of the main repository.
Our in JIRA. If you want to join say hello at Slack channel. If you are working on a ticket make sure to get the JIRA access, assign it to yourself, and update the ticket status to In Progress).
In Konsist all checks are tailored for a given project.
As the project grows additional checks can be defined to enforce various checks eg. layer boundaries, package structure, class naming, and more. The following sections contain a set of sample checks to give an idea of what is achievable with Konsist.
Most of the snippets are wrapped as JUnit tests, however, these snippets can be easily wrapped in the kotest tests.
Konsist ecosystem compatibility
Konsist is compatible with all types of Kotlin projects including Android, Spring, and Kotlin Multiplatform projects.
Konsist works with popular testing frameworks executing Kotlin code. Konsist has first-class support for JUni4, JUnit5, and Kotest.
Konsist is compatible with popular build systems such as Gradle and Maven.
The Java 8 is a minimum Java version required to run Konsist.
Konsist is backwards compatible with Kotlin 1.8.x ( since Konsist 0.17.0 ).
Konsist depends on:
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4 (minimal coroutine usage make Konsist compatible with newer coroutines versions)
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.0.20
Browse the current list of contributors directly on GitHub.
📢 Let us know about issues you face and improvements you would like to see. Your feedback is crucial in shaping the future of Konsist. Whether it's a suggestion for improvement, a bug report, or a question about usage, we're here to listen and help.
Share your thoughts on the #konsist channel to get help or start a new GitHub discussion 💬 for bug reports, issues, and feature requests.
Select packages
Package wildcard syntax is used to provide a more flexible way of querying packages.
The two dots (..) means any zero or more packages eg. all classes reside in a package starting with com.app:
Konsist
.scopeFromProject()
.classes()
.assertTrue { it.resideInPackages("com.app..") }
// com.app.data - valid
// com.app.data.repository - valid
// com.data - invalid
// com - invalid
Package wildcard syntax can be used multiple times inside the string argument. Here all interfaces reside in a package logger prefixed and suffixed by any number of packages:
Konsist
.scopeFromProject()
.interfaces()
.assertTrue { it.resideInPackages("..logger..") }
// logger - valid
// com.logger - valid
// com.logger.tree - valid
// com - invalidBy default, JUnit tests are run sequentially in a single thread. To speed up tests parallel execution can be enabled.
Create junit-platform.properties a file containing:
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=0.95Place this file in the resources directory of the test source set e.g:
src/test/resource/junit-platform.properties
or
src/konsistTest/resource/junit-platform.propertiesRead more in the official JUnit5 documentation.
@Test
fun `all generic return types contain X in their name`() {
Konsist
.scopeFromProduction()
.functions()
.returnTypes
.withGeneric()
.assertTrue { it.hasNameContaining("X") }
}RepositoryFor large projects with many classes to parse, the default JVM heap size might not suffice. If you encounter java.lang.OutOfMemoryError: Java heap space error consider increasing the maxHeapSize for the test source set:
Add the following argument to thebuild.gradle.kts file:
tasks.withType<Test> {
maxHeapSize = "1g"
}Add the following argument to the build.gradle file:
tasks.withType(Test).configureEach {
maxHeapSize = "1g"
}Add the following argument to the pom.xml file:
The source declaration (sourceDeclaration property) holds the reference to actual type declaration such as class or interface.
Konsist API allows for verify properties of such type e.g.:
Check if property type implements certain interface
Check if function return type name ends with Repository
Check if parent class is annotated with given annotation
Every declaration that is using another type such as property, function, parent exposes sourceDeclaration property.
Let's look at few examples:
Check if type of current property is has a type which is a class declaration heaving internal modifier:
Check if function return type is a basic Kotlin type:
Konsist can be used to guard the consistency of classes related to the [Kotlin Serialization](https://kotlinlang. org/docs/serialization.html) library.
Serializable Have All Properties Annotated With SerialName@Test
fun `classes annotated with 'Serializable' have all properties annotated with 'SerialName'`() {
Konsist
.scopeFromProject()
.classes()
.withAnnotationOf(Serializable::class)
.properties()
.assertTrue {
it.hasAnnotationOf(SerialName::class)
}
}Serializable Have All Enum Constants Annotated With SerialNameThe indirectParents parameter (parents(), hasParentClass(), hasAllParentInterfacesOf methods etc.). specifies whether or not to retrieve parent of the parent (indirect parents). By default, indirectParents is false e.g.
For above inheritance hierarchy is possible to retrieve:
Direct parents of ClassC (ClassB):
All parents present in the codebase hierarchy (ClassB and ClassC):
Notice that only parents existing in the project code base are returned.
Some code constructs can be represented as declarations (declaration-site) and as properties (use-site).
Consider this annotation class:
The above code represents the declaration of the CustomLogger annotation class, the place in the code where this annotation is declared (declaration-site). This declaration can be retrieved by filtering KoScope declarations...
Konsist + Kotest
Konsist has first-class support for meaning that every following release will be developed with Kotest compatibility in mind. API has been improved to support Kotest flows. However, Konsist cannot automatically retrieve Kotest test names, meaning the test name won't appear in error logs upon test failure. To fully utilize Konsist with Kotest, you must explicitly provide the test name.
Konsist can't obtain the test name from all dynamic tests (including tests).
It's recommended to provide the test name using the testName parameter. Supplying a test name provides additional benefits:
What is declaration?
The declaration (KoDeclaration) represents a code entity, a piece of Kotlin code. Every parsed Kotlin File (KoFileDeclaration) contains one or more declarations. The declaration can be a package (KoPackageDeclaration), property (KoPropertyDeclaration), annotation (KoAnnotationDeclaration), class (KoClassDeclaration), etc.
Consider this Kotlin code snippet file:
The above snippet is represented by the KoFileDeclarationclass. It contains two declarations - property declaration (KoPropertyDeclaration
Boost command line output
When running using non- the default command line output contains only the test name:
To be able to see full exception log containing invalid declaration file path and line number enable exceptionFormat in Gradle testLogging:
Now log output provides all informations relevant to pin point the invalid declaration:
Stay up to date
The full change log is available in the .
We appreciate your interest in Konsist. If you find our project valuable, please consider supporting Konsist through . Every contribution, no matter how small, adds up to make a significant impact.
Your personal sponsorship helps us continue our work and improve the tool for the entire community.
@Test
fun `property generic type does not contains star projection`() {
Konsist
.scopeFromProduction()
.properties()
.types
.assertFalse { type ->
type
.typeArguments
?.flatten()
?.any { it.isStarProjection }
}
}If your organization has open source funding initiatives, we'd be grateful if you could recommend Konsist or connect us with the appropriate team. Corporate backing can significantly boost our ability to enhance and sustain the project, benefiting the entire user community. For corporate sponsorship inquiries, please contact us at [email protected].
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<argLine>-Xmx1g</argLine>
</configuration>
</plugin>@Test
fun `all generic return types contain Kotlin collection type argument`() {
Konsist
.scopeFromProduction()
.functions()
.returnTypes
.withGeneric()
.typeArguments
.assertTrue { it.sourceDeclaration?.isKotlinCollectionType }
}@Test
fun `enum classes annotated with 'Serializable' have all enum constants annotated with 'SerialName'`() {
Konsist.scopeFromProject()
.classes()
.withEnumModifier()
.withAnnotationOf(Serializable::class)
.enumConstants
.assertTrue { it.hasAnnotationOf(SerialName::class) }
}@Test
fun `all models are serializable`() {
Konsist
.scopeFromPackage("com.myapp.model..")
.classes()
.assertTrue {
it.hasAnnotationOf(Serializable::class)
}
}class UseCaseTest : FreeSpec({
"UseCase has test class" {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue(testName = this.testCase.name.testName) { it.hasTestClasses() }
}
})class UseCaseTests : FreeSpec({
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.forEach { useCase ->
"${useCase.name} should have test" {
useCase.assertTrue(testName = this.testCase.name.testName) { it.hasTestClasses() }
}
"${useCase.name} should reside in ..domain..usecase.. package" {
useCase.assertTrue(testName = this.testCase.name.testName) { it.resideInPackage("..domain..usecase..") }
}
"${useCase.name} should ..." {
// another Konsist assert
}
}
})tasks.withType<Test> {
testLogging {
events(TestLogEvent.FAILED)
exceptionFormat = TestExceptionFormat.FULL
}
}tasks.test {
testLogging {
events(TestLogEvent.FAILED)
exceptionFormat = TestExceptionFormat.FULL
}
}Now consider this function:
The above code also contains CustomLogger annotation. However, this time code represents the place in the code where the annotation is used (use-site). Such annotations can be accessed using the annotations property:
Such properties can be used to check if the function annotated with CustomLogger annotation has the correct name prefix:
annotation class CustomLoggerkoScope
.classes()
.withAnnotationModifier()// Every annotation class must reside in the "annotation" package
koScope
.classes()
.withAnnotationModifier()
.assertTrue { it.resideInPackage("..annotation..") }The appropriate test names will appear in the log if the test fails.
Test suppression will be facilitated (See Suppress Konsist Test)
Kotest enables fetching the test name from the context to populate the testName argument, ensuring consistent naming of tests:
To facilitate test name retrieval you can add a custom koTestName extension:
This extension enables more concise syntax for providing Kotest test name:
@Test
fun `every api declaration has KDoc`() {
Konsist
.scopeFromPackage("..api..")
.declarationsOf<KoKDocProvider>()
.assertTrue { it.hasKDoc }
}@Test
fun `every class has test`() {
Konsist
.scopeFromProduction()
.classes()
.assertTrue { it.hasTestClass() }
}@Test
fun `every class - except data and value class - has test`() {
Konsist
.scopeFromProduction()
.classes()
.withoutModifier(KoModifier.DATA, KoModifier.VALUE)
.assertTrue { it.hasTestClass() }
}@Test
fun `2 layer architecture has correct dependencies`() {
Konsist
.scopeFromProject()
.assertArchitecture {
val presentation = Layer("Presentation", "com.myapp.presentation..")
val business = Layer("Business", "com.myapp.business..")
val persistence = Layer("Persistence", "com.myapp.persistence..")
val database = Layer("Database", "com.myapp.database..")
presentation.dependsOn(business)
business.dependsOn(presentation)
business.dependsOn(persistence)
persistence.dependsOn(business)
business.dependsOn(database)
database.dependsOn(business)
}
}@Test
fun `every file in module reside in module specific package`() {
Konsist
.scopeFromProject()
.files
.assertTrue { it.packagee?.name?.startsWith(it.moduleName) }
}@Test
fun `function parameter has generic type argument with name ending with 'Repository'`() {
Konsist
.scopeFromProduction()
.functions()
.parameters
.types
.withGeneric()
.sourceDeclarations()
.assertFalse { it.hasNameEndingWith("Repository") }
}// Code Snippet
internal class Engine
val current: Engine? = null
// Konsist test
Konsist
.scopeFromProject()
.properties()
.assertTrue {
it
.type
?.sourceDeclaration
?.asClassDeclaration()
?.hasInternalModifier // true
}
// Code Snippet
internal class Engine {
fun start(): Boolean
}
// Konsist test
Konsist
.scopeFromProject()
.classes()
.functions()
.assertTrue {
it.returnType?
.sourceDeclaration
?.isKotlinBasicType
}// Code Snippet
internal class Engine {
fun start(): Boolean
}
// Konsist test
Konsist
.scopeFromProject()
.classes()
.parents()
.assertTrue {
it
.sourceDeclaration
?.isInterface
}> Task :konsistTest:test
UseCaseKonsistTest > every use case has single public operator function named 'invoke' FAILED
com.lemonappdev.konsist.core.exception.KoAssertionFailedException at UseCaseKonsistTest.kt:26
2 tests completed, 1 failed
> Task :konsistTest:testDebugUnitTest FAILED
FAILURE: Build failed with an exception.
> Task :konsistTest:test
UseCaseKonsistTest > every use case has single public operator function named 'invoke' FAILED
com.lemonappdev.konsist.core.exception.KoAssertionFailedException: Assert 'every use case has single public operator function named 'invoke'' was violated (25 times). Invalid declarations:
/myproject/usecase/LoginUserUseCase.kt:6:1 (LoginUserUseCase ClassDeclaration)
/myproject/usecase/GetLocationUseCase.kt:8:1 (GetLocationUseCase ClassDeclaration)
2 tests completed, 1 failed
> Task :konsistTest:testDebugUnitTest FAILED
FAILURE: Build failed with an exception.
@CustomLogger
fun logHello() {
println("Hello")
}koScope
.functions()
.annotations// Every function with a name starting with "log" is annotated with CustomLogger
koScope
.functions()
.withAllAnnotations("CustomLogger")
.assertTrue {
it.hasNameStartingWith("log")
}class UseCaseTest : FreeSpec({
"useCase test" {
Konsist
.scopeFromProject()
.classes()
.assertTrue (testName = this.testCase.name.testName) { }
}
})val TestScope.koTestName: String
get() = this.testCase.name.testNameclass UseCaseTest : FreeSpec({
"useCase test" {
Konsist
.scopeFromProject()
.classes()
.assertTrue (testName = koTestName) { } // extension used
}
})@Test
fun `every function with parameters has a param tags`() {
Konsist.scopeFromPackage("..api..")
.functions()
.assertTrue { it.hasValidKDocParamTags() }
}@Test
fun `every function with return value has a return tag`() {
Konsist.scopeFromPackage("..api..")
.functions()
.assertTrue { it.hasValidKDocReturnTag() }
}@Test
fun `every extension has a receiver tag`() {
Konsist.scopeFromPackage("..api..")
.declarationsOf<KoReceiverTypeProvider>()
.assertTrue { it.hasValidKDocReceiverTag() }
}@Test
fun `every public function in api package must have explicit return type`() {
Konsist
.scopeFromPackage("..api..")
.functions()
.assertTrue { it.hasReturnType() }
}@Test
fun `every public property in api package must have specify type explicitly`() {
Konsist
.scopeFromPackage("..api..")
.properties()
.assertTrue { it.hasType() }
}@Test
fun `test classes should have test subject named sut`() {
Konsist
.scopeFromTest()
.classes()
.assertTrue {
val type = it.name.removeSuffix("Test")
val sut = it
.properties()
.firstOrNull { property -> property.name == "sut" }
sut != null && (sut.type?.name == type || sut.text.contains("$type("))
}
}@Test
fun `test classes should have all members private besides tests`() {
Konsist
.scopeFromTest()
.classes()
.declarations()
.filterIsInstance<KoAnnotationProvider>()
.withoutAnnotationOf(Test::class, ParameterizedTest::class, RepeatedTest::class)
.filterIsInstance<KoVisibilityModifierProvider>()
.assertTrue { it.hasPrivateModifier }
}@Test
fun `files reside in package that is derived from module name`() {
Konsist.scopeFromProduction()
.files
.assertTrue {
/*
module -> package name:
feature_meal_planner -> mealplanner
feature_caloric_calculator -> caloriccalculator
*/
val featurePackageName = it
.moduleName
.removePrefix("feature_")
.replace("_", "")
it.hasPackage("com.myapp.$featurePackageName..")
}
}KoClassDeclarationLoggerKoFunctionDeclarationDeclarations mimic the Kotlin file structure. Konsts API provides a way to retrieve every element. To get all functions in all classes inside the file using .classes().functions() :
Each declaration contains a set of properties to facilitate filtering and verification eg. KoClass declaration has name, modifiers , annotations , declarations (containing KoFunction) etc. Here is how the name of the function can be retrieved.
Although it is possible to retrieve a property of a single declaration usually verification is performed on a collection of declarations matching certain criteria eg. methods annotated with specific annotations or classes residing within a single package. See the Declaration Filtering page.
Each declaration exposes a few additional properties to help with debugging:
text - provides declaration text eg. val property role = "Developer"
location - provides file path with file name, line, and column e.g. ~\Dev\IdeaProject\SampleApp\src\kotlin\com\sample\Logger:10:5
locationWithText - provides location together with the declaration text
Konsist
.scopeFromProject()
.classes()
.first { it.name == "ClassC" }
.parents() // ClassBKonsist
.scopeFromProject()
.classes()
.first { it.name == "SampleClass" }
.parents(indirectParents = true) // ClassB, ClassARetrofitting Konsist into a project that hasn't followed strict structural guidelines can pose initial challenges, necessitating a thoughtful approach to smoothly transition without disrupting ongoing development. Unlike most linters, which provide a baseline file, Konsist follows a different methodology (for now).
The baseline file will be added in the future.
There are two approaches that can be employed when retrofitting Konsist into an existing projectAdd Konsist Existing To Project (Baseline) and Suppress Annotation.
Scope represents a set of Kotlin files. The scope allows to verification of all Kotlin files in the project or only a subset of the project code base.
When refactoring an existing application, you can either choose to first refactor a module and then add a Konsist test or initially add the Konsist test to identify errors, followed by the necessary refactor. Both strategies aim to ensure modules align with Konsist's structural guidelines.
Consider this The MyDiet application with feature 3 modules:
At first, the Konsist test can be applied to a single module:
As refactoring proceeds and code gets aligned, the Konsist scope can be extended to another feature module (featureGroceryListGenerator):
When entire code base (all modules) are aligned with the Konsist tests, the scope can be retrieved from the entire project:
Usage of project scope (scopeFromProject ) is a recommended approach because it helps to guard future modules without modifying the existing Konsist test.
Konsist provides a flexible API to create scopes from modules, source sets, packages, files, etc., and combine these scopes together. See .
The second approach, Suppress Annotation, may be helpful when to Konsist swiftly without making substantial alterations to the existing kotlin files. See .
The primary focus of Konsist API is to reflect the state of the Kotlin source code (that will be verified), not the state of the running program. The Kotlin compiler has a deeper understanding of the Kotlin code base than Konsist, because the compiler can infer more information. Let's take a look at a few examples.
Consider this class:
Is Logger class public? Yes obviously it is public, however, the hasPublicModifier method returns the false value:
Why is that? The public visibility modifier is the default visibility modifier in Kotlin. Meaning that class will be public even if it does not have the explicit public modifier. Since the class has no public modifier the hasPublicModifier method returns false. To distinguish between class being public and class having explicitpublic modifier Konsist API provides another method to retrieve declaration visibility:
Let's look at the name property:
The name property is obviously of the String type. However String type is inferred, so Konsist has no way of Knowing the actual type (in this exact case this is achievable, but with more complex expressions containing delegates, setters, getters, or methods this approach would not work).
Let's look at the primary constructor for the same class:
The Logger the class has a primary constructor because the Kotlin compiler will generate a parameterless constructor under the hood. However, the Konsist API will return a null value because the primary constructor is not present in the Kotlin source code:
Consider this function:
Kotlin will infer String as the return type of the getName function. Since the source code does not contain this explicit return type Konsist lacks information about the return type. In this scenario, hasReturnType the method will return the false value:
Unlike the previous example, Konsist has no way to determine the actual function return type.
Konsist occasionally releases snapshot versions to a dedicated snapshot repository. These snapshots provide early access to new features and bug fixes.
Snapshot versions are development builds and may contain unstable features.
Currently, snapshots are released manually. At some point this process will be automated - new snapshot will be released each time code is merged to develop branch
First, you need to include the snapshot repository in your project configuration. Here's how to do it for different build systems:
Add the following dependency to the module\pom.xml file:
To use Konsist SNAPSHOT dependency changing version to X.Y.Z-SNAPSHOT (versions can be found in ):
Add the following dependency to the module\build.gradle.kts file:
Add the following dependency to the module\build.gradle file:
Add the following dependency to the module\pom.xml file:
Code snippets employed to ensure the uniformity of tests written with JUnit library.
Test Annotation Should Have Test Suffix@Test
fun `classes with 'Test' Annotation should have 'Test' suffix`() {
Konsist
.scopeFromSourceSet("test")
.classes()
.filter {
it.functions().any { func -> func.hasAnnotationOf(Test::class) }
}
.assertTrue { it.hasNameEndingWith("Tests") }
}The annotation serves as a powerful tool to control lines and static analysis tools. When writing the Konsist test, there might be instances where the specific guard is not applicable due to certain project-specific reasons. The @Suppress annotation can be used to ignore those particular issues, ensuring that the codebase still adheres to the overall linting standards
In Konsist the @Suppress annotation parameter name is derived from the name of the test to be suppressed. For example - this test verifies if every API declaration has KDoc:
The name of the test is every api declaration has KDoc, so we can suppress this test by using one of these arguments:
Konsist enables development teams to enforce structural rules for interfaces ensuring code consistency across projects.
To verify interfaces start by querying all interface present in the project:
Konsist allows you to verify multiple aspects of a interfaces. For a complete understanding of the available APIs, refer to the language reference documentation for .
Let's look at few examples.
Functions can be validated for their signatures, modifiers, naming patterns, return types, and parameter structures.
To verify functions start by querying all functions present in the project:
In practical scenarios you'll typically want to verify a specific subset of functions - such as those defined inside classes:
Konsist API allows to query local functions:
Konsist allows you to verify multiple aspects of a functions. For a complete understanding of the available APIs, refer to the language reference documentation for .
Let's look at few examples.
Properties can be checked for proper access modifiers, type declarations, and initialization patterns.
To verify properties start by querying all properties present in the project:
In practical scenarios you'll typically want to verify a specific subset of properties - such as those defined inside classes:
Konsist allows you to verify multiple aspects of a properties. For a complete understanding of the available APIs, refer to the language reference documentation for KoPropertyDeclaration.
Let's look at few examples.
Where are we now?
The Konsist linter has undergone extensive field testing across a variety of projects, including , , and , and is compatible with both and build systems. Additionally, Konsist features a comprehensive test suite with around 5,000 tests, running on various operating systems including MacOS, Windows, and Ubuntu, to minimize the risk of regressions.
Konsist is safe for use because it is not bundled with the production code (only included in the test source sets).
Over the next few months, we will fix bugs, improve existing APIs, and implement missing features (in this order). Here is a high-level roadmap:
Many linters including and have a predefined set of rules. These rules are derived and aligned with guidelines or common practices for writing high-quality code and industry coding conventions (, , etc.).
However, there are no industry standards when comes to application architecture. Every code base is different - different class names, different package structures, different application layers, etc. As the project grows code base evolves as well - it tends to have more layers, more modules, and a more complex code structure. These "rules" are hard to capture by generic linter, because they are often specific to the given project.
Let's consider a use case - a concept defined by the . At a high level the use case definition is quite simple - "use case holds a business logic". How the use case is represented in the code base? Well... In one project this may be a class that has a name ending with UseCase, in another, it may be a class extending BaseUseCase and in another class annotated with @UseCase annotation. The logic for filtering "all project use cases" will vary from project to project.
private const val logLevel = "debug"
@Entity
open class Logger(val level: String) {
fun log(message: String) {
}
}koFile // List<KoFile>
.classes() // List<KoClassDeclaration>
.functions() // List<KoFunctionDeclaration>val name = koFile // List<KoFileDeclaration>
.classes() // List<KoClassDeclaration>
.functions() // List<KoFunctionDeclaration>
.first() // KoFunctionDeclaration
.name // String
println(name) // prints: logclass Logger✅ Milestone 1 (Q1-Q2-Q3 2023)
✅ Setup GitHub project
✅ Setup CI pipeline
✅ Core Library development
✅ Publish artifact to Maven Central
✅ Create documentation
✅ Internal closed testing Android
✅ Internal closed testing Spring
✅ Milestone 2 (Q4 2023 Alpha)
✅ Community-driven testing
✅ Improve existing APIs
✅ Fix Bugs
✅ Polish documentation and samples
✅ Implement new features
✅ Milestone 3 (Q1 2024)
✅ Stabilize APIs (minimal breaking changes)
✅ Fix Bugs
✅ Polish documentation and samples
✅ Implement new features
🚀 Milestone 4 (Q2 2024)
✅ Declaration references
🚀 Milestone 5 (Q3 2024)
🚀 Architecture checks improvements
🚀 Bug fixes
🚀 API improvements
🚀 Milestone 6 (Q4 2024 Beta)
🚀 Bug fixes
🚀 API improvements
🚀 Release 1.0
🕝 Milestone 5 (H1 2025)
🕝 Further maintenance and improvements
should every use case have UseCase a suffix in the class name?
can the use case be extended or include another use case?
should every use case reside in usecase the package?
should the use case have a single method?
how this method should be named?
can this method have overloads or should it be defined as invoke an operator?
should this method have a suspended modifier?
…
Answers will vary from project to project. That is why Konsist favors a more flexible approach - it allows filtering members and defining custom code base assertions (tests). On top of that Konsist is utilizing Kotlin collection processing API to provide more control over filtering and asserting declarations (Declaration).
Some things can be standardized across different projects e.g. constructor parameter names being derived from the property name, or alphabetic order of the parameter. For now, custom tests will be a core part of Konsist, however, we are considering the addition of a small set of predefined rules in the future.

repositories {
// Konsist snapshot repository
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
// More repositorues
}repositories {
// Konsist snapshot repository
maven {
url 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
}
// More repositories
}<repositories>
<!-- Konsist snapshot repository -->
<repository>
<id>konsist-snapshots</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<!-- More repositories -->
</repositories>@Test
fun `test classes should have test subject named sut`() {
Konsist
.scopeFromTest()
.classes()
.assertTrue {
// Get type name from test class e.g. FooTest -> Foo
val type = it.name.removeSuffix("Test")
val sut = it
.properties()
.firstOrNull { property -> property.name == "sut" }
sut != null && sut.hasTacitType(type)
}
}@Suppress("every api declaration has KDoc")The name of the actual Konsist test was prefixed by konsist - @Suppress("konsist.every api declaration has KDoc"). This is helpful if you have multiple lines in the project and want to know which linters own a given check (check that needs to be suppressed)
In the below example, @Suppress annotation is applied to author property:
When using @Suppress annotation, it's advisable to apply it to the smallest possible scope to ensure that only the intended warnings are suppressed, so other potential issues aren't inadvertently overlooked. In the above example, the @Suppress annotation was applied to the property.
If broader suppression is necessary, you can then escalate to the interface level:
As a last resort, if multiple elements in a file need the same suppression, the @Suppress annotation can be applied to the entire file:
Konsist has no way of retrieving the name of the current Kotest test (unlike JUnit).
To allow suppression (and correct test names) it is recommended to utilize the name derived from the Kotest context using the testName argument:
To suppress such tests use the test name prefixed with konsist.:
Interface names can be validated to ensure they follow project naming conventions and patterns.
Check if interface name ends with Repository:
Interface modifiers can be validated to ensure proper encapsulation and access control.
Check if interface has internal modifier:
Interface-level and member annotations can be verified for presence, correct usage, and required attribute values.
Check if interface is annotated with Service annotation:
Package declarations can be validated to ensure classes are located in the correct package structure according to architectural guidelines.
Check if interface has model package or sub-packages (.. means include sub-packages):
Methods can be validated for their signatures, modifiers, annotations, naming patterns, return types, and parameter structures.
Check if methods (functions defined inside interface) have name starting with Local:
See .
Properties can be checked for proper access modifiers, type declarations, and initialization patterns.
Check if all properties (defined inside interface) has val modifiers:
See Verify Properties.
Generic type parameters and constraints can be checked for correct usage and bounds declarations.
Check if interface has not type parameters:
Generic type arguments can be checked for correct usage.
Check if parent has no type arguments:
Inheritance hierarchies, interfaces implementations, and superclass relationships can be validated.
Check if interface extends CrudRepository:
Companion object declarations, their contents, and usage patterns can be verified for compliance.
Check if interface has companion object:
The sequential arrangement of interface members can be enforced according to defined organizational rules.
Check if interface properties are defined before functions:
Function names can be validated to ensure they follow project naming conventions and patterns.
Check if function name starts with get :
Function modifiers can be validated to ensure proper encapsulation and access control.
Check if function has public or default (also public) modifier:
Function-level and member annotations can be verified for presence, correct usage, and required attribute values.
Check if function is annotated with Binding annotation:
Functions with block bodies (using curly braces) can be validated to ensure compliance with code structure requirements:
Expression body functions (using single-expression syntax) can be verified to maintain consistent style across the codebase:
Function parameters can be validated for their types, names, modifiers, and annotations to ensure consistent parameter usage.
Check if function has parameter of type String:
Return types can be checked to ensure functions follow expected return type patterns and contracts.
Check if function has Kotlin collection type:
Generic type parameters can be validated to ensure proper generic type usage and constraints.
Check if function has type parameters:
Generic type arguments can be checked for correct usage.
Check if return type has no type arguments:
Top-level functions (functions not declared inside a class) can be specifically queried and validated:
This helps ensure top-level functions follow project conventions, such as limiting their usage or enforcing specific naming patterns.
ViewModelProperty names can be validated to ensure they follow project naming conventions and patterns.
Check if Boolean property has name starting with is:
Property types can be validated to ensure type safety and conventions:
Property modifiers can be validated to ensure proper encapsulation:
Property annotations can be verified for presence and correct usage:
Getter and setter presence and implementation can be validated:
Check if property has getter:
Check if property has setter:
Property initialization can be verified:
Property delegates can be verified:
Check if property has lazy delegate:
Property visibility scope can be validated:
Check if property has internal modifier:
Property mutability can be checked.
Check if property is immutable:
Check if property is mutable:
Konsist
.scopeFromModule("featureCaloryCalculator")
.classes()
.assertTrue { it.hasTestClasses() }Konsist
.scopeFromModule("featureCaloryCalculator", "featureGroceryListGenerator")
.classes()
.assertTrue { it.hasTest() }Konsist
.scopeFromProject()
.classes()
.assertTrue { it.hasTest() }koClass.hasPublicModifier() // falsekoClass.isPublicOrDefault() // trueprivate val name = Stringclass LoggerkoClass.primaryConstructor // nullfun getName() = "Konsist"koFunction.hasReturnType() // falsedependencies {
testImplementation("com.lemonappdev:konsist:X.Y.Z-SNAPSHOT")
}dependencies {
testImplementation "com.lemonappdev:konsist:X.Y.Z-SNAPSHOT"
}<dependency>
<groupId>com.lemonappdev</groupId>
<artifactId>konsist</artifactId>
<version>X.Y.Z-SNAPSHOT</version>
<scope>test</scope>
</dependency>@Test
fun `test classes should have all members private besides tests`() {
Konsist
.scopeFromTest()
.classes()
.declarations()
.filterIsInstance<KoAnnotationProvider>()
.withoutAnnotationOf(Test::class, ParameterizedTest::class, RepeatedTest::class)
.filterIsInstance<KoVisibilityModifierProvider>()
.assertTrue { it.hasPrivateModifier }
}@Test
fun `no class should use JUnit4 Test annotation`() {
Konsist
.scopeFromProject()
.classes()
.functions()
.assertFalse {
it.annotations.any { annotation ->
annotation.fullyQualifiedName == "org.junit.Test"
}
}
}fun `every api declaration has KDoc`() {
// ...
}package com.myapp.api
/**
* Represents a simple `Book` entity.
*/
interface Book {
/**
* The title of the book.
*/
val title: String
@Suppress("konsist.every api declaration has KDoc")
val author: String
}package com.myapp.api
/**
* Represents a simple `Book` entity.
*/
@Suppress("konsist.every api declaration has KDoc")
interface Book {
/**
* The title of the book.
*/
val title: String
val author: String
}@file:Suppress("konsist.every api declaration has KDoc")
package com.myapp.api
/**
* Represents a simple `Book` entity.
*/
interface Book {
/**
* The title of the book.
*/
val title: String
@Suppress("konsist.every api declaration has KDoc")
val author: String
}package com.api.test
class UseCaseTest : FreeSpec({
"useCase test" {
Konsist
.scopeFromProject()
.classes()
.assertTrue (testName = this.testCase.name.testName) { }
}
})package com.api.controller
@Suppress("konsist.useCase test")
class MyUseCase {
... // code
}Konsist
.scopeFromProject()
.interfaces()
.....
.assertTrue {
it.hasNameEndingWith("Repository")
}..
.assertTrue {
it.hasInternalModifier
}...
.assertTrue {
it.hasAnnotationOf(Service::class)
}...
.assertTrue {
it.resideInPackage("com.lemonappdev.model..")
}...
.functions()
.assertTrue {
it.hasNameStartingWith("Local")
}...
.properties()
.assertTrue {
it.isVal
}...
.assertFalse {
it.hasTypeParameters()
}...
.parents()
.assertFalse {
it.hasTypeArguments()
}...
.assertTrue {
it.hasParentOf(CrudRepository::class)
}...
.assertTrue {
it.hasObject { objectt -> objectt.hasCompanionModifier }
}...
.assertTrue {
val lastKoPropertyDeclarationIndex = it
.declarations(includeNested = false, includeLocal = false)
.indexOfLastInstance<KoPropertyDeclaration>()
val firstKoFunctionDeclarationIndex = it
.declarations(includeNested = false, includeLocal = false)
.indexOfFirstInstance<KoFunctionDeclaration>()
if (lastKoPropertyDeclarationIndex != -1 && firstKoFunctionDeclarationIndex != -1) {
lastKoPropertyDeclarationIndex < firstKoFunctionDeclarationIndex
} else {
true
}
}Konsist
.scopeFromProject()
.functions()
...Konsist
.scopeFromProject()
.classes()
.functions()
...Konsist
.scopeFromProject()
.classes()
.functions(includeLocal = true)
......
.assertTrue {
it.hasNameStartingWith("get")
}..
.assertTrue {
it.hasPublicOrDefaultModifier
}...
.assertTrue {
it.hasAnnotationOf(Binding::class)
}...
.assertTrue {
it.hasBlockBody
}...
.assertTrue {
it.hasExpressionBody
}...
.assertTrue {
it.hasParameter { parameter -> parameter.hasTypeOf(String::class) }
}...
.assertTrue {
it.returnType?.sourceDeclaration?.isKotlinCollectionType
}...
.assertTrue {
it.hasTypeParameters()
}...
.assertFalse {
it.returnType?.hasTypeArguments()
}...
.assertTrue {
it.isTopLevel
}@Test
fun `classes extending 'ViewModel' should have 'ViewModel' suffix`() {
Konsist
.scopeFromProject()
.classes()
.withParentClassOf(ViewModel::class)
.assertTrue { it.name.endsWith("ViewModel") }
}@Test
fun `Every 'ViewModel' public property has 'Flow' type`() {
Konsist
.scopeFromProject()
.classes()
.withParentClassOf(ViewModel::class)
.properties()
.assertTrue {
it.hasPublicOrDefaultModifier && it.hasType { type -> type.name == "kotlinx.coroutines.flow.Flow" }
}
}@Test
fun `'Repository' classes should reside in 'repository' package`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("Repository")
.assertTrue { it.resideInPackage("..repository..") }
}@Test
fun `no class should use Android util logging`() {
Konsist
.scopeFromProject()
.files
.assertFalse { it.hasImport { import -> import.name == "android.util.Log" } }
}@Test
fun `All JetPack Compose previews contain 'Preview' in method name`() {
Konsist
.scopeFromProject()
.functions()
.withAnnotationOf(Preview::class)
.assertTrue {
it.hasNameContaining("Preview")
}
}@Test
fun `every class with Serializable must have its properties Serializable`() {
val message =
"""In Android, every serializable class must implement the Serializable interface
|or be a simple non-enum type because this is how the Java and Android serialization
|mechanisms identify which objects can be safely converted to a byte stream for
|storage or transmission, ensuring that complex objects can be properly reconstructed
|when deserialized.""".trimMargin()
Konsist
.scopeFromProduction()
.classes()
.withParentNamed("Serializable")
.properties()
.types
.sourceDeclarations()
.withoutKotlinBasicTypeDeclaration()
.withoutClassDeclaration { it.hasEnumModifier }
.assertTrue(additionalMessage = message) {
it.asClassDeclaration()?.hasParentWithName("Serializable")
}
}Konsist
.scopeFromProject()
.properties()
...Konsist
.scopeFromProject()
.classes()
.properties()
......
.assertTrue {
it.type?.name == "Boolean" && it.hasNameStartingWith("is")
}...
.assertTrue {
it.type?.name == "LocalDateTime"
}...
.assertTrue {
it.hasLateinitModifier
}...
.assertTrue {
it.hasAnnotationOf(JsonProperty::class)
}...
.assertTrue {
it.hasGetter
}...
.assertTrue {
it.hasSetter
}...
.assertTrue {
it.isInitialized
}...
.assertTrue {
it.hasDelegate("lazy")
}...
.assertTrue {
it.isInternal
}...
.assertTrue {
it.isVal
}...
.assertTrue {
it.isVar
}Verify codebase using Konsist API
Assertions are used to perform code base verification. This is the final step of Konsist verification preceded by scope creation (Create The Scope) and Declaration Filtering steps:
Konsist offers a variety of assertion methods. These can be applied to a list of KoDeclarations as well as a single declaration.
In the below snippet, the assertion (performed on the list of interfaces) verifies if every interface has a public visibility modifier.
The it parameter inside the assertTrue method represents a single declaration (single interface in this case). However, the assertion itself will be performed on every available interface. The last line in the assertTrue block will be evaluated as true or false providing the result for a given asset.
The assertFalse is a negation of the assertTrue method. In the below snippet, the assertion (performed on the list of properties) verifies if none of the properties has the Inject annotation:
This assertion verifies that the class does not contain any properties with public (an explicit public modifier) or default (implicit public modifier) modifiers:
This assertion helps to verify if the given list of declarations is empty.
This assertion helps to verify if the given list of declarations is not empty.
Assertions offer a set of parameters allowing to tweak the assertion behavior. You can adjust several settings, such as setting testName that helps with suppression (see ).
You can also enable enhanced verification by setting strict argument to true:
The additionalMessage param allows to provision of additional messages that will be displayed with the failing test. This may be a more detailed description of the problem or a hint on how to fix the issue.
Konsist enables development teams to enforce structural rules for class ensuring code consistency across projects.
To verify classes start by querying all classes present in the project:
Konsist allows you to verify multiple aspects of a class. For a complete understanding of the available APIs, refer to the language reference documentation for KoClassDeclaration.
Let's look at few examples.
Class names can be validated to ensure they follow project naming conventions and patterns.
Check if class name ends with Repository:
Class modifiers can be validated to ensure proper encapsulation and access control.
Check if class has internal modifier:
Class-level and member annotations can be verified for presence, correct usage, and required attribute values.
Check if class is annotated with Service annotation:
Package declarations can be validated to ensure classes are located in the correct package structure according to architectural guidelines.
Check if class has model package or sub-packages (.. means include sub-packages):
Methods can be validated for their signatures, modifiers, annotations, naming patterns, return types, and parameter structures.
Check if methods (functions defined inside class) have no annotations:
See .
Properties can be checked for proper access modifiers, type declarations, and initialization patterns.
Check if all properties (defined inside class) has val modifiers:
See .
Primary and secondary constructors can be validated for parameter count, types, and proper initialization.
Check if class has explicit primary constructor:
Check if primary constructor is annotated with Inject annotation:
Generic type parameters and constraints can be checked for correct usage and bounds declarations.
Check if class has not type parameters:
Generic type arguments can be checked for correct usage.
Check if parent has no type arguments:
Inheritance hierarchies, interfaces implementations, and superclass relationships can be validated.
Check if class extends CrudRepository:
Companion object declarations, their contents, and usage patterns can be verified for compliance.
Check if class has companion object:
The sequential arrangement of class members can be enforced according to defined organizational rules.
Check if class properties are defined before functions:
Type parameter vs type argument
To undersigned Konsist API let's look at the difference between generic type parameters and generic type arguments:
Type Parameter is the placeholder (like T) you write when creating a class or function (declaration site)
Type Argument is the actual type (like String or Int) you provide when using that class or function (use site)
Simple Examples:
Type parameters can be defined, for example, inside class or function.
UiState:type parameters has out modifier:Service:The flatten() extension method allows to flatten type parameters structure:
For a type argument like String, it returns listOf().
For a type argument like List<String>, it returns listOf(String).
For a type argument like Map<List<String>, Int>, it returns
UIState:Konsist provides preconfigured sample projects. Each project contains a complete build script config and a simple Konsist test. Projects are available in the starter-projects directory. Each JUnit5 and Kotest project has an additional dynamic test (Dynamic Konsist Tests)(dynamic tests are currently available at the develop branch).
Android
Static
Static + Dynamic
Static + Dynamic
JUnit 4
JUnit 5
JUnit 5
JUnit5
Kotest
Understand whats going on
To gain insight into the inner workings of the Konsist test, examine the data provided by the Konsist API.
Two primary tools can help you comprehend the inner workings of the Konsist API are Debug Konsist Test and Print To Console.
The IntelliJ IDEA / Android Studio provides a handy feature called Evaluate Expressions which is an excellent tool for debugging Konsist tests.
Create a simple test class and click on the line number to add the :
Debug the test:
When the program stops at the breakpoint (blue line background) run Evaluate Expression... action...
...or press Evaluate Expression... button:
In the Evaluate window enter the code and click the Evaluate the button. For example, you can list all of the classes present in the scope to get the class names:
You can also display a single-class declaration to view its name:
Konsist provides a flexible API that allows to output of the specified data as console logs. Scopes, lists of declarations, and single declarations can all be printed.
Print a list of files from KoScope:
Print multiple declarations:
Print a given attribute for each declaration:
Print single declaration:
Print list of queried declarations before and after query:
Print nested declarations:
The Konsist project is licensed under Apache License-2.0.
Third-party libraries, plugins, and tools that Konsist project uses:
See file for more details.
Konsist's Architectural Checks serve as a robust tool for maintaining layer isolation, enabling development teams to enforce strict boundaries between different architectural layers. Here few things that can be verified with Konsist:
domain layer is independant
data layer depends on domain layer
Konsist
.scopeFromProject()
.classes()
...Spring
Static
Static + Dynamic
Static + Dynamic
Kotlin Multiplatform
Static
Static + Dynamic
Static + Dynamic
listOf("List, String, Int)




UseCasedomainusecase...
.assertTrue {
it.hasNameEndingWith("Repository")
}...
.assertTrue {
it.hasInternalModifier
}...
.assertTrue {
it.hasAnnotationOf(Service::class)
}...
.assertTrue {
it.resideInPackage("com.lemonappdev.model..")
}...
.functions()
.assertTrue {
it.annotations.isEmpty()
}...
.properties()
.assertTrue {
it.isVal
}...
.assertTrue {
it.hasPrimaryConstructor
}...
.primaryConstructors
.assertTrue {
it.hasAnnotation(Inject::class)
}...
.assertFalse {
it.hasTypeParameters()
}...
.parents()
.assertFalse {
it.hasTypeArguments()
}...
.assertTrue {
it.hasParentOf(CrudRepository::class)
}...
.assertTrue { declaration ->
declaration.hasObject { it.hasCompanionModifier }
}...
.assertTrue {
val lastKoPropertyDeclarationIndex = it
.declarations(includeNested = false, includeLocal = false)
.indexOfLastInstance<KoPropertyDeclaration>()
val firstKoFunctionDeclarationIndex = it
.declarations(includeNested = false, includeLocal = false)
.indexOfFirstInstance<KoFunctionDeclaration>()
if (lastKoPropertyDeclarationIndex != -1 && firstKoFunctionDeclarationIndex != -1) {
lastKoPropertyDeclarationIndex < firstKoFunctionDeclarationIndex
} else {
true
}
}// Example 1: Class
// Here 'T' is a TYPE PARAMETER
class Box<T>(val item: T)
// Here 'String' is a TYPE ARGUMENT
val stringBox = Box<String>("Hello")
// Example 2: Function
// Here 'T' is a TYPE PARAMETER
fun <T> printWithType(item: T) {
println("Type is: ${item::class.simpleName}")
}
// Here 'String' and 'Int' are TYPE ARGUMENTS
printWithType<String>("Hello") // prints: Type is: String// Code Snippet
class View<UiState>(val state: UiState) // UiState is typeParamener
// Konsist
Konsist
.scopeFromProject()
.classes()
.typeParameters // access type parameters
.assertTrue {
it.name == "UiState" // true
}//Code Snippet
fun <out T> setState(item: T?) {
// ...
}
// Konsist
Konsist
.scopeFromProject()
.functions()
.typeParameters // access type parameters
.assertTrue {
it.hasOutModifier // true
}//Code Snippet
val services: List<Service> = emptyList()
// Konsist
Konsist
.scopeFromProject()
.properties()
.assertTrue { property ->
property
.type
?.typeArguments
?.flatten()
?.any { typeArgument -> typeArgument.name == "Service" }
}// Snippet
fun setState(uiState: View<WelcomeUIState>)
// Konsist Test
Konsist
.scopeFromProject()
.properties()
.parameters
.types
.typeArguments
.assertTrue {
it.hasNameEndingWith("UIState") // true
}// Snippet
open class Container<T>(private val item: T) { }
class StringContainer(text: String) : Container<String>(text) { }
// Konsist Test
Konsist
.scopeFromProject()
.classes()
.parents()
.typeArguments
.flatten()
.assertTrue {
it.name == "String" // true
}koScope
.classes()
.first()
.namekoScope // KoScope
.print()koScope
.classes() // List<KoClassDeclaration>
.print()koScope
.classes() // List<KoClassDeclaration>
.print { it.fullyQualifiedName }koScope
.classes() // List<KoClassDeclaration>
.first() // KoClassDeclaration
.print()koScope
.classes() // List<KoClassDeclaration>
.print(prefix = "Before") // or .print(prefix = "Before") { it.name }
.withSomeAnnotations("Logger")
.print(prefix = "After") // or .print(prefix = "After") { it.name }koScope
.classes() // List<KoClassDeclaration>
.constructors // List<KoConstructorDeclaration>
.parameters // List<KoParameterDeclaration>
.print()@Test
fun `clean architecture layers have correct dependencies`() {
Konsist
.scopeFromProduction()
.assertArchitecture {
// Define layers
val domain = Layer("Domain", "com.myapp.domain..")
val presentation = Layer("Presentation", "com.myapp.presentation..")
val data = Layer("Data", "com.myapp.data..")
// Define architecture assertions
domain.dependsOnNothing()
presentation.dependsOn(domain)
data.dependsOn(domain)
}
}@Test
fun `classes with 'UseCase' suffix should reside in 'domain' and 'usecase' package`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue { it.resideInPackage("..domain..usecase..") }
}@Test
fun `classes with 'UseCase' suffix should have single 'public operator' method named 'invoke'`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue {
val hasSingleInvokeOperatorMethod = it.hasFunction { function ->
function.name == "invoke" && function.hasPublicOrDefaultModifier && function.hasOperatorModifier
}
hasSingleInvokeOperatorMethod && it.countFunctions { item -> item.hasPublicOrDefaultModifier } == 1
}
}@Test
fun `classes with 'UseCase' suffix and parents should have single 'public operator' method named 'invoke'`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue {
// Class and it's parent
val declarations = listOf(it) + it.parents(true)
// Functions from all parents without overrides
val uniqueFunctions = declarations
.mapNotNull { koParentDeclaration -> koParentDeclaration as? KoFunctionProvider }
.flatMap { koFunctionProvider ->
koFunctionProvider.functions(
includeNested = false,
includeLocal = false
)
}
.filterNot { koFunctionDeclaration -> koFunctionDeclaration.hasOverrideModifier }
val hasInvokeOperatorMethod = uniqueFunctions.any { functionDeclaration ->
functionDeclaration.name == "invoke" && functionDeclaration.hasPublicOrDefaultModifier && functionDeclaration.hasOperatorModifier
}
val numParentPublicFunctions = uniqueFunctions.count { functionDeclaration ->
functionDeclaration.hasPublicOrDefaultModifier
}
hasInvokeOperatorMethod && numParentPublicFunctions == 1
}
}@Test
fun `interfaces with 'Repository' annotation should reside in 'data' package`() {
Konsist
.scopeFromProject()
.interfaces()
.withAnnotationOf(Repository::class)
.assertTrue { it.resideInPackage("..data..") }
}@Test
fun `every UseCase class has test`() {
Konsist
.scopeFromProduction()
.classes()
.withNameEndingWith("UseCase")
.assertTrue { it.hasTestClasses() }
}Gradle Test Logger Plugin
Apache-2.0 License
JUnit
Eclipse Public License - v 2.0
Kotest
Apache-2.0 License
Gradle
Apache-2.0 License
Ktlint
MIT License
Detekt
Apache-2.0 License
Dokka
Apache-2.0 License
Kotlin
Apache-2.0 License
Kotlin-compiler
Apache-2.0 License
Mockk
Apache-2.0 License
Spotless
Apache-2.0 License
...
Let's write a simple test to verify that application architecture rules are preserved. In this scenario, the application follows a simple 3-layer architecture, where Presentation and Data layers depend on Domain layer and Domain layer is independant (from these layers):
On a high level writing Konsist architectural check requires 3 steps:
Let's take a closer look at each of these steps.
Create layers instances to represent project layers. Each Layer instance accepts the name (used for presenting architecture violation errors) and package used to define layers.
The Konsist object is an entry point to the Konsist library.
The scopeFromX methods obtains the instance of the scope containing Kotlin project files. To get all Kotlin project files present in the project use the scopeFromProject method:
To performa assertion use the assertArchiteture method:
Utilize dependsX methods to validate that your project's layers adhere to the defined architectural dependencies:
Wrap Konsist Code In Test
The declaration validation logic should be protected through automated testing. By wrapping Konsist checks within standard testing frameworks such as JUnit or KoTest, you can verify these rules with each Pull Request:
Note that test class has a KonsistTest suffix. This is the recommended approach to name classes containing Konsist tests.
This section described the basic way of writing Konsist architectural test. To get a better understanding of how Konsist API works see Debug Konsist Test.
Konsist Declaration Checks provide a powerful mechanism for validating the structural elements of the Kotlin codebase. These checks allow developers to enforce structural rules and coding conventions by verifying classes, interfaces, functions, properties, and other code declarations. Here few things that can be verified with Konsist:
All Use cases should reside in usecase specific package
Repository classes must implement Repository interface
All repository classes should have name ending with Repository
data classes should have only val properties
Test classes should have test subject named sut
...
Let's write a simple test to verify that all classes (all class declarations) residing in resides in controller package are annotated with the RestController annotation .
On a high level writing Konsist declaration check requires 4 steps:
Let's take a closer look at each of these steps.
The first step is to get a list of Kotlin files to be verified.
The Konsist object is an entry point to the Konsist library.
The scopeFromX methods obtains the instance of the scope containing Kotlin project files. To get all Kotlin project files present in the project use the scopeFromProject method:
Each file in the scope contains set of declarations like classes, properties functions etc. (see ). To write this declaration check for all classes present in the scope query classes using classes method :
In this project controllers are defined as classes annotated with RestController annotation. Use withAllAnnotationsOf method to filter classes with with RestController annotation:
To performa assertion use the assertTrue method:
To verify that classes are located in the controller package, use the resideInPackage method inside assertTrue block:
This verification applies to the entire collection of previously filtered classes, rather than examining just one class in isolation.
The declaration validation logic should be protected through automated testing. By wrapping Konsist checks within standard testing frameworks such as or , you can verify these rules with each :
Note that test class has a KonsistTest suffix. This is the recommended approach to name classes containing Konsist tests.
This section described the basic way of writing Konsist declaration test. To get a better understanding of how Konsist API works see and sections.
The above test will execute multiple assertions per test (all controllers will be verified in a single test). If you prefer better isolation each assertion can be executed as a separate test. See the page.
For dynamic tests, Konsist can't obtain the current test's name. Test name may be correctly displayed in the IDE, however, the testName argument should be provided to enable:
Correct test names are displayed in the log when the test is failing
Test suppression (See Suppress Konsist Test)
The testName argument should be passed to assertX methods such as assertTrue , assertFalse etc. Let's look at the code:
Here is the summary of test frameworks:
Here is a concrete implementation passing he testName argument for each test Framework:
introduced native support for dynamic tests, however, it also supports static tests. For static test testName does not have to be passed as it can be internally retrieved by Konsist.
introduced native support for dynamic tests, allowing tests to be generated at runtime through the @TestFactory annotation.
provides robust support for dynamic tests, allowing developers to define test cases programmatically at runtime, making it a flexible alternative to traditional JUnit testing. It is recommended to utilize the name derived from the Kotest (this.testCase.name.testName) context as the value for the testName argument:
Verify codebase using Konsist API
Architecture assertions are used to perform architecture verification. It is the final step of Konsist verification preceded by scope creation (Create The Scope):
As an example, this simple 2-layer architecture will be used:
The assertArchitecture block defines architecture layer rules and verifies that the layer requirements are met.
Create class instance to represent project layers. Each Layer instance accepts the name (used for presenting architecture violation errors) and package used to define architectural layer:
The final step is to define the dependencies (relations) between each layer using one of these methods:
dependsOn
dependsOnNothing
doesNotDependOn
The above methods follow up the layer definitions inside assertArchitecture block:
By default dependsOn method works like does not perform strict layer validation (strict = false). However this behaviour is controlled b ystrict parameter:
strict = false (default) - may depend on layer
strict = true - have to depend on layer
e.g.
Architecture verification can be performed on KoScope (as seen above) and a list containing KoFiles. For example, you can remove a few files from the scope before performing an architectural check:
This approach provides more flexibility when working with complex projects, however, The desired approach is to create a dedicated scope. See .
The method allows to include layer in architecture verification, without defining a dependency for this layer:
Architecture configuration can be defined beforehand and stored in a variable to facilitate checks for multiple scopes:
This approach may be helpful when refactoring existing applications. To facilitate readability the above checks should be expressed as two unit tests:
class ArchitectureKonsistTest {
@Test
fun `architecture layers have dependencies correct`() {
Konsist
.scopeFromProject()
.assertArchitecture {
private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
private val domainLayer = Layer("Domain", "com.myapp.business..")
private val dataLayer = Layer("Data", "com.myapp.data..")
// Define layer dependnecies
presentationLayer.dependsOn(domainLayer)
dataLayer.dependsOn(domainLayer)
domainLayer.dependsOnNothing()
}
}
}class ArchitectureKonsistTest {
class UseCaseTest : FreeSpec({
"architecture layers have dependencies correct" {
Konsist
.scopeFromProject()
.assertArchitecture {
private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
private val domainLayer = Layer("Domain", "com.myapp.business..")
private val dataLayer = Layer("Data", "com.myapp.data..")
// Define layer dependnecies
presentationLayer.dependsOn(domainLayer)
dataLayer.dependsOn(domainLayer)
domainLayer.dependsOnNothing()
}
}
})
}// Define layers
private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
private val domainLayer = Layer("Domain", "com.myapp.domain..")
private val dataLayer = Layer("Data", "com.myapp.data..")Konsist// Define layers
private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
private val domainLayer = Layer("Domain", "com.myapp.domain..")
private val dataLayer = Layer("Data", "com.myapp.data..")
// Define the scope containing all Kotlin files present in the project
Konsist.scopeFromProject() //Returns KoScope// Define layers
private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
private val domainLayer = Layer("Domain", "com.myapp.domain..")
private val dataLayer = Layer("Data", "com.myapp.data..")
Konsist
.scopeFromProject()
// Assert architecture
.assertArchitecture {
// Define architectural rules
}Konsist
.scopeFromProject()
.assertArchitecture {
private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
private val domainLayer = Layer("Domain", "com.myapp.business..")
private val dataLayer = Layer("Data", "com.myapp.data..")
// Define layer dependnecies
presentationLayer.dependsOn(domainLayer)
dataLayer.dependsOn(domainLayer)
domainLayer.dependsOnNothing()
}koScope
.interfaces()
.assertTrue { it.hasPublicModifier() }Konist
.scopeFromProject()
.properties()
.assertFalse {
it.hasAnnotationOf(Inject::class)
}Konist
.scopeFromProject()
.properties()
.assertFalse {
it.hasPublicOrDefaultModifier
}Konist
.scopeFromProject()
.classes()
.assertEmpty()Konist
.scopeFromProject()
.classes()
.assertNotEmpty()Konist
.scopeFromProject()
.classes()
.assertFalse(strict = true) { ... }Konist
.scopeFromProject()
.classes()
.assertFalse(additionalMessage = "Do X to fix the issue") { ... }Konsist
.scopeFromProject()
.assertArchitecture {
// Assert architecture
}dynamic
Recommended
To facilitate test name retrieval you can add this custom koTestName extension:
JUnit 4 does not natively support dynamic tests; tests in this framework are typically static and determined at compile-time, so there is no need to pass testName argument.
JUnit4
static
Not required
JUnit5
static
Not required
JUnit5
dynamic
Recommended
@Test
fun myTest() {
Konsist.scopeFromProject()
.classes()
.assertTrue { ... }
}class SampleDynamicKonsistTest {
@TestFactory
fun `use case test`(): Stream<DynamicTest> = Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.stream()
.flatMap { useCase ->
Stream.of(
dynamicTest("${useCase.name} should have test") {
useCase.assertTrue(testName = "${useCase.name} should have test") {
it.hasTestClass()
}
},
dynamicTest("${useCase.name} should reside in ..domain.usecase.. package") {
useCase.assertTrue(testName = "${useCase.name} should reside in ..domain.usecase.. package") {
it.resideInPackage("..domain.usecase..")
}
},
)
}
}Kotest
@Test
fun myTest() {
Konsist.scopeFromProject()
.classes()
.assertTrue { ... }
}Konsist.scopeFromProject()
.classes()
.assertTrue(testName = "My test name") { ... } //passed test nameclass SampleDynamicKonsistTest : FreeSpec({
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.forEach { useCase ->
"${useCase.name} should have test" {
useCase.assertTrue(testName = this.testCase.name.testName) { it.hasTestClass() }
}
"${useCase.name} should reside in ..domain.usecase.. package" {
useCase.assertTrue(testName = this.testCase.name.testName) { it.resideInPackage("..domain.usecase..") }
}
}
})val TestScope.koTestName: String
get() = this.testCase.name.testNameThe Kotest testing framework project dependency should be added to the project. See starter projects to get a complete sample project.
From static to dynamic
On this page, we explore the domain of static tests and then progress to the flexible world of dynamic tests. As a starting point, let's dive into the traditional approach of static Konsist tests.
With static tests, the failure is represented by a single test:
From this failure, a developer discerns the breached rule and needs to dive into the test logs to determine the cause of the violation (to pinpoint the use case breaking the given rule).
In contrast, dynamic tests immediately highlight the root issue since every use case is represented by its own distinct test:
Utilizing dynamic tests over static ones makes it simpler to pinpoint failures. Consequently, it reduces the time and effort spent on parsing long error logs, offering a more efficient testing experience.
Let's begin by creating a static test and then delve into the steps to transition towards dynamic tests.
Static tests are defined at compile-time. This means the structure and number of these tests are fixed when the code is compiled. When navigating the universe of Konsist tests, the standard approach is to execute several validations all bundled within a single test.
To paint a clearer picture: imagine you have a rule (let's represent it with the tool icon 🛠️) ensuring that all use cases should be placed in a specific package. One static test (represented by the check icon ✅) can guard this rule, making sure that everything is in the right place:
In most projects, the intricacy arises from a multitude of classes/interfaces, each with distinct duties. However, to simplify our understanding, let's use a straightforward and simplified example of a project with just three use cases:
The goal is to verify if every use case follows these two rules:
verify if every use case has a test
verify if every use case is in domain.usecase package
A typical approach would be to write two Konsist tests:
Each rule is represented as a separate test verifying all of the use cases:
Executing these tests will generate output in the IDE:
While the current setup using static, predefined tests is functional, dynamic tests offer an avenue for improved development experience and flexibility.
Dynamic tests are generated at runtime based on conditions and input data. In this scenario, the dynamic input data is the list of use cases that grows over the project life cycle.
The objective is to generate dynamic tests for each combination of rule and use case (KoClass declaration) verified by Konsist. With three use cases and two rules for each, this will yield a total of six separate tests:
Let's convert this idea into a dynamic test:
JUnit provides built-in support for dynamic tests through its core framework. This ensures that developers can seamlessly employ dynamic testing capabilities.
The IDE will display the tests as follows:
Konsist can be used to guard the consistency of the Spring project.
Repository Annotation Should Have Repository SuffixRestController Annotation Should Have Controller SuffixRestController Annotation Should Reside In controller PackageRestController Annotation Should Never Return CollectionAdmin Suffix Should Have PreAuthorize Annotation With ROLE_ADMINQuery and filter declarations using Konsist API
Declaration querying allows to retrieval of declarations of a given type. It is the middle step of the Konsist config preceded by scope retrieval (Create The Scope) and followed by the verification (Declaration Assertion) step.
Typically, verification has performed a collection of declarations such as methods marked with particular annotations or classes located within a single package.
Every Create The Scope contains a set of declarations (Declaration) such as classes (KoClass), properties (KoProperty), functions (KoFunction), etc. The KoScope class provides a set of properties and methods to access Kotlin declarations. Each of them returns a list representing a declaration subset:
To get all classes from the given scope use KoScope.classes() method:
Here is an example of querying all properties defined inside classes:
More granular filtering can be applied to additionally filter classes annotated with certain attributes like classes annotated with UseCase annotation.
Konsist is compatible with API, so the filter method can be used to filter the content of the List<KoClass>: Here filter return classes annotated with UseCase annotation:
Konsist provides a set of with... extensions to simplify the filtering syntax. The above snippet can be improved:
Multiple conditions can be chained to perform more specific filtering. The below snippet filters classes with the BaseUseCase parent class that resides in the usecase package:
It is also possible to filter declarations by using certain aspects e.g. visibility modifiers. Usage of providers allows verifying the visibility of different declaration types such as classes, functions, properties, etc:
Querying and filtering stages can be mixed to perform more specific checks. The below snippet filters classes reside in the controller package retrieves all properties, and filters properties with Inject annotation:
To print all declarations within use the print() method:
Konsist // Define the scope containing all Kotlin files present in the project
Konsist.scopeFromProject() //Returns KoScopeKonsist.scopeFromProject()
// Get scope classes
.classes()
Konsist.scopeFromProject()
.classes()
// Filter classes annotated with 'RestController'
.withAllAnnotationsOf(RestController::class) Konsist.scopeFromProject()
.classes()
.withAllAnnotationsOf(RestController::class)
.assertTrue {
// Define the assertion
} Konsist.scopeFromProject()
.classes()
.withAllAnnotationsOf(RestController::class)
.assertTrue {
// Check if classes are located in the controller package
it.resideInPackage("..controller")
} class ControllerClassKonsistTest {
@Test
fun `classes annotated with 'RestController' annotation reside in 'controller' package`() {
// 1. Create a scope representing the whole project (all Kotlin files in project)
Konsist.scopeFromProject()
// 2. Retrieve class declarations
.classes()
// 3. Filter classes annotated with 'RestController'
.withAllAnnotationsOf(RestController::class)
// 4. Define the assertion
.assertTrue { it.resideInPackage("..controller..") }
}
}class ControllerClassKonsistTest : FreeSpec({
"classes annotated with 'RestController' annotation reside in 'controller' package" {
Konsist
// 1. Create a scope representing the whole project (all Kotlin files in project)
.scopeFromProject()
// 2. Retrieve class declarations
.classes() // 2. Get scope classes
// 3. Filter classes annotated with 'RestController'
.withAllAnnotationsOf(RestController::class)
// 4. Define the assertion
.assertTrue (testName = this.testCase.name.testName) {
it.resideInPackage("..controller..")
}
}
})Konsist
.scopeFromProject()
.assertArchitecture {
// Define layers
val presentation = Layer("Presentation", "com.myapp.presentation..")
val data = Layer("Data", "com.myapp.data..")
}Konsist
.scopeFromProject()
.assertArchitecture {
val presentation = Layer("Presentation", "com.myapp.presentation..")
val data = Layer("Data", "com.myapp.data..")
// Define dependencies
presentation.dependsOn(data)
data.dependsOnNothing()
}// Optional dependency - Feature layer may depend on Domain layer
featureLayer.dependsOn(domainLayer) // strict = false by default
// Required dependency - Feature layer must depend on Domain layer
featureLayer.dependsOn(domainLayer, strict = true)Konsist
.scopeFromProject()
.files
.withNameStartingWith("Repository")
.assertArchitecture {
val presentation = Layer("Presentation", "com.myapp.presentation..")
val data = Layer("Data", "com.myapp.data..")
presentation.dependsOn(data)
data.dependsOnNothing()
}private val domain = Layer("Domain", "com.domain..")
private val presentation = Layer("Presentation", "com..presentation..")
Konsist
.scopeFromProject()
scope.assertArchitecture {
// Include presentation for architectural check without defining a dependency
presentation.include()
// Include domain layer or architectural check and define no dependency (independent)
domain.doesOnNothing()
}
}// Define architecture
val architecture = architecture {
val presentation = Layer("Presentation", "com.myapp.presentation..")
val data = Layer("Data", "com.myapp.data..")
presentation.dependsOn(data)
data.dependsOnNothing()
}
// Assert Architecture of two modules using common architecture rules
moduleFeature1Scope.assertArchitecture(architecture)
moduleFeature2Scope.assertArchitecture(architecture)class ArchitectureTest {
private val architecture = architecture {
val presentation = Layer("Presentation", "com.myapp.presentation..")
val data = Layer("Data", "com.myapp.data..")
presentation.dependsOn(data)
data.dependsOnNothing()
}
@Test
fun `architecture layers of feature1 module have dependencies correct`() {
moduleFeature1Scope.assertArchitecture(architecture)
}
@Test
fun `architecture layers of feature2 module have dependencies correct`() {
moduleFeature2Scope.assertArchitecture(architecture)
}
}@Test
fun `interfaces with 'Repository' annotation should have 'Repository' suffix`() {
Konsist
.scopeFromProject()
.interfaces()
.withAnnotationOf(Repository::class)
.assertTrue { it.hasNameEndingWith("Repository") }
}@Test
fun `classes with 'RestController' annotation should have 'Controller' suffix`() {
Konsist
.scopeFromProject()
.classes()
.withAnnotationOf(RestController::class)
.assertTrue { it.hasNameEndingWith("Controller") }
}@Test
fun `controllers never returns collection types`() {
/*
Avoid returning collection types directly. Structuring the response as
an object that contains a collection field is preferred. This approach
allows for future expansion (e.g., adding more properties like "totalPages")
without disrupting the existing API contract, which would happen if a JSON
array were returned directly.
*/
Konsist
.scopeFromPackage("story.controller..")
.classes()
.withAnnotationOf(RestController::class)
.functions()
.assertFalse { function ->
function.hasReturnType { it.isKotlinCollectionType }
}
}@Test
fun `classes with 'RestController' annotation should reside in 'controller' package`() {
Konsist
.scopeFromProject()
.classes()
.withAnnotationOf(RestController::class)
.assertTrue { it.resideInPackage("..controller..") }
}@Test
fun `classes with 'RestController' annotation should never return collection`() {
Konsist
.scopeFromPackage("story.controller..")
.classes()
.withAnnotationOf(RestController::class)
.functions()
.assertFalse { function ->
function.hasReturnType { it.hasNameStartingWith("List") }
}
}@Test
fun `Service classes should be annotated with Service annotation`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("Service")
.assertTrue { it.hasAnnotationOf(Service::class) }
}@Test
fun `Entity classes should have an Id field`() {
Konsist
.scopeFromProject()
.classes()
.withAnnotationOf(Entity::class)
.assertTrue { clazz ->
clazz.properties().any { property ->
property.hasAnnotationOf(Id::class)
}
}
}@Test
fun `DTO classes should be data classes`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("DTO")
.assertTrue { it.hasModifier(KoModifier.DATA) }
}@Test
fun `RestControllers should not have state fields`() {
Konsist
.scopeFromProject()
.classes()
.withAnnotationOf(RestController::class)
.objects()
.withModifier(KoModifier.COMPANION)
.assertTrue {
it.properties().isEmpty()
}
}@Test
fun `files with domain package do not have Spring references`() {
Konsist.scopeFromProduction()
.files
.withPackage("..domain..")
.assertFalse {
it
.imports
.any { import ->
import.name.startsWith("org.springframework")
}
}
}@Test
fun `Transactional annotation should only be used on default or public methods that are not part of an interface`() {
Konsist.scopeFromProject()
.functions()
.withAnnotationOf(Transactional::class)
.assertTrue {
it.hasPublicOrDefaultModifier && it.containingDeclaration !is KoInterfaceDeclaration
}
}@Test
fun `every API method in RestController with 'Admin' suffix should have PreAuthorize annotation with ROLE_ADMIN`() {
Konsist.scopeFromProject()
.classes()
.withAnnotationOf(RestController::class)
.withNameEndingWith("Admin")
.functions()
.assertTrue {
it.hasAnnotationOf(PreAuthorize::class) && it.text.contains("hasRole('ROLE_ADMIN')")
}
}@Test
fun `every non-public Controller should have @PreAuthorize on class or on each endpoint method`() {
Konsist.scopeFromProject()
.classes()
.withAnnotationOf(RestController::class)
.filterNot { it.hasPublicModifier }
.assertTrue { controller ->
controller.hasAnnotationOf(PreAuthorize::class) ||
controller.functions()
.all { it.hasAnnotationOf(PreAuthorize::class) }
}
}For dynamic tests such as JUnit 5, it is recommended that the test name is explicitly provided using testName argument (see Explicit Test Names). At the moment test names are duplicated. This aspect has to be further investigated.
Kotest offers native support for JUnit's dynamic tests. Developers can effortlessly integrate and utilize dynamic testing features without needing additional configurations or plugins.
The IDE will display the tests as follows:
In JUnit 4, the concept of dynamic tests (like JUnit 5's @TestFactory) does not exist natively thus dynamic tests are not supported.





returns all functions present in the scope
properties()
returns all properties present in the scope
typeAliases
returns all type aliases present in the scope
declarations()
returns all declarations present in the scope
Annotation1Annotation2.Method
Description
files
returns all files present in the scope
packages
returns all packages present in the scope
imports
returns all imports present in the scope
classes()
returns all classes present in the scope
interfaces()
returns all interfaces present in the scope
objects()
returns all objects present in the scope
functions()
ext Package Must Have Name Ending With Extvaluem PrefixforbiddenString In FileDeclaration reference represents a link between codebase declarations. Konsist allows to precisely verify properties of linked type. This type can be used in function or property declaration or child/parent class or interface. For example
1. Verify if all types of function parameters are interfaces:
2. Access properties of parents (parent classes and child interfaces). Below snippet checks if parent class has internal modifier:
3. Access properties of children (child classes and child interfaces). Below snippet checks if all interfaces have children that resided in ..somepackage.. package:
Kotlin types can defined in multiple ways. Consider foo property with Foo type:
The Foo type can be defined by:
class
interface
object
type alias
The Foo type can be represented by one of KoXDeclaration classes:
Each of these types possesses a largely distinct set of characteristics; for instance, classes and interfaces can include annotations, whereas import aliases cannot.
To access properties the specific declaration type, the declaration cast to more specific type is required (from generic KoTypeDeclaration). Example below assumes that Foo is represented by the Foo class:
To facilitate testing Konsist API provides set of dedicated casting extensions. The above code can be simplified:
Here is the list of all casting extensions:
Source code:
Usage:
Konsist test:
Source code:
Usage:
Konsist test:
This scenario is uncommon, but still possible.
Source code:
Usage:
Konsist test:
Source code:
Usage:
Konsist test:
Source code:
Usage:
Konsist test:
Source code:
Usage:
Konsist test:
Source code:
Usage:
Konsist test:
External type represents the type defined outside of the project codebase, usually by external library. Konsist is not able to parse this type, so type information is limited (Konsist is not able to parse the compiled file).
For Example:
The Android ViewModel class is provided by androidx.lifecycle:lifecycle-viewmodel-ktx dependency, so Konsist has limited information.
class UseCaseKonsistTest : FreeSpec({
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.forEach { useCase ->
"${useCase.name} should have test" {
useCase.assertTrue(testName = this.testCase.name.testName) { it.hasTestClass() }
}
"${useCase.name} should reside in ..domain.usecase.. package" {
useCase.assertTrue(testName = this.testCase.name.testName) { it.resideInPackage("..domain..usecase..") }
}
}
})class UseCaseKonsistTest {
@Test
fun `use case should have test`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue { it.hasTestClass() }
}
@Test
fun `use case reside in domain dor usecase package`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue { it.resideInPackage("..domain..usecase..") }
}
}class UseCaseKonsistTest : FreeSpec({
val useCases = Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
"use case should have test" {
useCases.assertTrue(testName = this.testCase.name.testName) { it.hasTestClass() }
}
"use case should reside in ..domain.usecase.. package" {
useCases.assertTrue(testName = this.testCase.name.testName) { it.resideInPackage("..domain.usecase..") }
}
})class UseCaseKonsistTest {
@TestFactory
fun `use case test`(): Stream<DynamicTest> = Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.stream()
.flatMap { useCase ->
Stream.of(
dynamicTest("${useCase.name} should have test") {
useCase.assertTrue(testName = "${useCase.name} should have test") {
it.hasTestClass()
}
},
dynamicTest("${useCase.name} should reside in ..domain.usecase.. package") {
useCase.assertTrue(testName = "${useCase.name} should reside in ..domain.usecase.. package") {
it.resideInPackage("..domain.usecase..")
}
},
)
}
}koScope
.classes() koScope
.classes()
.properties()
.assertTrue {
//...
}koScope
.classes()
.filter { it.hasAnnotationOf<UseCase>() }
.assertTrue {
//...
}koScope
.classes()
.withAllAnnotationsOf(UseCase::class)
.assertTrue {
//...
}koScope
.classes()
.withAllAnnotationsOf(UseCase::class)
.withPackage("..usecase")
.assertTrue {
//...
}koScope
.declarationsOf<KoVisibilityModifierProvider>()
.assertTrue { it.hasInternalModifier }koScope
.classes() // query all classes
.withPackage("..controller") // filter classes in 'controller' package
.properties() // query all properties
.withAnnotationOf(Inject::class) // filter classes in 'controller' package
.assertTrue {
//...
}koScope
.classes()
.properties()
.print()@Test
fun `files in 'ext' package must have name ending with 'Ext'`() {
Konsist
.scopeFromProject()
.files
.withPackage("..ext..")
.assertTrue { it.hasNameEndingWith("Ext") }
}Konsist
.scopeFromProject()
.functions()
.parameters
.types
.assertTrue {
it.isInterface
}fun `all parrent interfaces are internal`() {
Konsist
.scopeFromProject()
.classes()
.parentInterfaces()
.assertTrue {
it.hasInternalModifier()
}
}Konsist
.scopeFromProject()
.interfaces()
.assertTrue {
it.hasAllChildren(indirectChildren = true) { child ->
child.resideInPackage("..somepackage..")
}
}@Test
fun `all data class properties are defined in constructor`() {
Konsist
.scopeFromProject()
.classes()
.withModifier(KoModifier.DATA)
.properties()
.assertTrue { it.isConstructorDefined }
}@Test
fun `every class has test`() {
Konsist
.scopeFromProduction()
.classes()
.assertTrue { it.hasTestClasses() }
}@Test
fun `every class - except data and value class - has test`() {
Konsist
.scopeFromProduction()
.classes()
.withoutModifier(KoModifier.DATA, KoModifier.VALUE)
.assertTrue { it.hasTestClasses() }
}@Test
fun `properties are declared before functions`() {
Konsist
.scopeFromProject()
.classes()
.assertTrue {
val lastKoPropertyDeclarationIndex = it
.declarations(includeNested = false, includeLocal = false)
.indexOfLastInstance<KoPropertyDeclaration>()
val firstKoFunctionDeclarationIndex = it
.declarations(includeNested = false, includeLocal = false)
.indexOfFirstInstance<KoFunctionDeclaration>()
if (lastKoPropertyDeclarationIndex != -1 && firstKoFunctionDeclarationIndex != -1) {
lastKoPropertyDeclarationIndex < firstKoFunctionDeclarationIndex
} else {
true
}
}
}@Test
fun `every constructor parameter has name derived from parameter type`() {
Konsist
.scopeFromProject()
.classes()
.constructors
.parameters
.assertTrue {
val nameTitleCase = it.name.replaceFirstChar { char -> char.titlecase(Locale.getDefault()) }
nameTitleCase == it.type.sourceType
}
}@Test
fun `every class constructor has alphabetically ordered parameters`() {
Konsist
.scopeFromProject()
.classes()
.constructors
.assertTrue { it.parameters.isSortedByName() }
}@Test
fun `enums has alphabetically ordered consts`() {
Konsist
.scopeFromProduction()
.classes()
.withAllModifiers(KoModifier.ENUM)
.assertTrue { it.enumConstants.isSortedByName() }
}@Test
fun `companion object is last declaration in the class`() {
Konsist
.scopeFromProject()
.classes()
.assertTrue {
val companionObject = it.objects(includeNested = false).lastOrNull { obj ->
obj.hasModifier(KoModifier.COMPANION)
}
if (companionObject != null) {
it.declarations(includeNested = false, includeLocal = false).last() == companionObject
} else {
true
}
}
}@Test
fun `every value class has parameter named 'value'`() {
Konsist
.scopeFromProject()
.classes()
.withValueModifier()
.primaryConstructors
.assertTrue { it.hasParameterWithName("value") }
}@Test
fun `no empty files allowed`() {
Konsist
.scopeFromProject()
.files
.assertFalse { it.text.isEmpty() }
}@Test
fun `no field should have 'm' prefix`() {
Konsist
.scopeFromProject()
.classes()
.properties()
.assertFalse {
val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false
it.name.startsWith('m') && secondCharacterIsUppercase
}
}@Test
fun `no class should use field injection`() {
Konsist
.scopeFromProject()
.classes()
.properties()
.assertFalse { it.hasAnnotationOf<Inject>() }
}@Test
fun `no class should use Java util logging`() {
Konsist
.scopeFromProject()
.files
.assertFalse { it.hasImport { import -> import.name == "java.util.logging.." } }
}@Test
fun `package name must match file path`() {
Konsist
.scopeFromProject()
.packages
.assertTrue { it.hasMatchingPath }
}@Test
fun `no wildcard imports allowed`() {
Konsist
.scopeFromProject()
.imports
.assertFalse { it.isWildcard }
}@Test
fun `forbid the usage of 'forbiddenString' in file`() {
Konsist
.scopeFromProject()
.files
.assertFalse { it.hasTextContaining("forbiddenString") }
}@Test
fun `all function parameters are interfaces`() {
Konsist
.scopeFromProject()
.functions()
.parameters
.types
.assertTrue { it.sourceDeclaration?.isInterface }
}@Test
fun `all parent interfaces are public`() {
Konsist
.scopeFromProject()
.classes()
.parentInterfaces()
.sourceDeclarations()
.interfaceDeclarations()
.assertTrue { it.hasPublicModifier }
}@Test
fun `return type of all functions are immutable`() {
Konsist
.scopeFromProject()
.functions()
.returnTypes
.assertFalse { it.isMutableType }
}kotlin types (Kotlin basic type or Kotlin collections type)
function type
external library (type defined outside project codebase) represents declaration which is not defined in the project
KoFunctionDeclaration
KoExternalDeclaration
KoTypeAliasDeclaration
asTypeAliasDeclaration
isTypeAlias
KoImportAliasDeclaration
asImportAliasDeclaration
isImportAlias
KoKotlinTypeDeclaration
asKotlinTypeDeclaration
isKotlinType
KoFunctionDeclaration
asFunctionTypeDeclaration
isFunctionType
KoExternalDeclaration
asExternalTypeDeclaration
isExternalType
KoClassDeclaration
KoInterfaceDeclaration
KoObjectDeclaration
KoTypeAliasDeclaration
KoImportAliasDeclaration
KoKotlinTypeDeclaration
KoClassDeclaration
asClassDeclaration
isClass
KoInterfaceDeclaration
asObjectDeclaration
isObject
KoObjectDeclaration
asInterfaceDeclaration
isInterface

Let's Improve Konsist Together
So you want to help? That's great!
The Konsist project is now at a critical stage where community input is essential to polish and mature it.
There are a variety of ways to contribute to the Konsit project:
Coding: This is the most common way to contribute. You can fix bugs or add new features.
Testing: You can help to improve the quality by testing the code and reporting bugs. This is a great way to get involved and help out maturing the project.
Documentation: You can help to improve the documentation by writing or editing documentation. This is a great way to help people understand how to use Konsist.
No matter how you choose to contribute, you will be making a valuable contribution to the open-source community.
Our in JIRA.
The best way to interact with the Konsist team is the dedicated channel (). If you want to help or need guidelines just say hello at Slack channel.
Tickets that can be grabbed by the community have a label. You can also work on another improvement or bug-fix, but this may require more alignment, for example, certain features and planned ahead, so the ticket should be completed within a given time period.
Get contributor JIRA access - send your email in DM to at .
Pick the ticket in JIRA
Assign it to yourself, and update the ticket status to In Progress
Fork repository (uncheck "Copy the main branch only")
Branch of branch
Implement the changes
Add tests (look around in codebase for similar code being tested)
Open draft with branch as target ( branch will be merged into the branch after the release)
The - repository contains Konsist documentation (this webpage).
Fork repository
Branch of branch
Make changes
Open with branch as a target
During the PR review, several types of checks are executed using (). These checks can also be executed locally using the following commands:
(runs )
./gradlew spotlessCheck - check the code using Spotless
./gradlew spotlessApply - check and fix code using Spotless (if possible)
Some of the project README files contain diagrams. For a diagram preview, it is recommended to install the .
To test the changes locally you can publish a SNAPSHOT artifact of the Konsist to the local maven repository:
After publishing a new artifact x.y.z-SNAPSHOT with the version number will appear in the local Maven repository:
The actual Konsist version is defined in the file. The SNAPSHOT suffix will be added automatically to the published artifact.
To use this artifact you have to add a local Maven repository to your project.
Every project contains a list of the repositories used to retrieve the dependencies. A local Maven repository has to be manually added to the project.
Add the following block to the build.gradle / build.gradle.kts file:
By default, the Maven project uses a local repository. If not add the following block to the module\pom.xml file:
Dependency can be added to other build systems as well. Check the section in the sonatype repository.
Now build scripts will use the local repository to resolve dependencies, however, the version of Konsist has to be updated to the SNAPSHOT version of the newly published artifact e.g.
com.lemonappdev:konsist:0.12.0-SNAPSHOT
Now build scripts will be able to resolve this newly published Konsist artifact.
IntelliJ IDEA UI provides a convenient way to check which version of Konsist is used by the project. Open the External Libraries section of Project view and search for Konsist dependency:
If during a build you encounter an error regarding No matching toolchains found then open Module Settings / Project Structure windows and set Java SDK to version e.g. 19.
You can install missing JDKs directly from IntelliJ IDEA - click on the Module SDK combo box and select +Add SDK.
If during the build you encounter an error regarding Could not determine the dependencies of null. then open File / Settings / Build, Execute, Deployment / Build Tools / Gradle window and set Java SDK to version 19.
Konsist contains multiple custom source sets (defined by the ) to provide better isolation between various types of tests:
test - tests related to generic Konsist API (everything except the architectureAssert)
apiTest - tests related to architectureAssert
integrationTest
We aim to test the majority of aspects within these source sets. However, certain kinds of checks require a dedicated test project. These projects are available in the directory on the Konsist repository.
The high-level view of Konsist architecture:
The repository contains this website. Create a fork of the repository, make changes using any text editor (e.g. ), and open the Pull Request targeting the main branch.
The section requires a different approach. To ensure the snippets remain valid and aligned with Konsist API, we store them within the of the repository. With every release, new snippet pages are generated from the and placed in the GitBook documentation ( repository).
Some snippets depend on classes/interfaces/annotations from external frameworks such as Spring Repository annotation or Android ViewModel class. To avoid coupling Konsist with these frameworks and allow snippet compilation, we store placeholder classes mimicking the full names of the external framework in . class e.g. .
Aim for better test separation.
Typically, it's advisable to consolidate all Konsist tests in a unified location. This approach is preferred because these tests are often designed to validate the structure of the entire project's codebase. There are three potential options for storing Konsist tests in project codebase:
✅
✅
✅
✅
Recommended approach is to use or a . These approaches allows to easily isolate Konsist tests from other types of tests e.g. separate unit tests from Konsist tests.
The Konsist library can be added to the project by adding the dependency on the existing test source set .
To execute tests run ./gradlew test command.
The downside of this approach is that various types of tests are mixed in test source set e.g. unit tests and Konsist tests.
This section demonstrates how to add the konsistTest test source directory inside the app module. This configuration is mostly useful for Spring and Kotlin projects.
This test directory will have a kotlin folder containing Kotlin code.
Use the Gradle built-in to define the konsistTest source set. Add a testing block to the project configuration:
Use the Gradle built-in to define the konsistTest source set. Add a testing block to the project configuration:
Use the to define the konsistTest test source directory. Add plugin config to the project configuration:
Create app/src/konsistTest/kotlin folder and reload the project. The IDE will present a new konsistTest source set in the app module.
The konsistTest test source folder works exactly like the build-in test source folder, so Kosist tests can be defined and executed in a similar way:
This section demonstrates how to add the konsistTest module to the project. This configuration is primarily helpful for Android projects and Kotlin Multiplatform (KMP) projects, however, this approach will also work with Spring and pure Kotlin projects.
konsistTest Module:Create konsistTest/src/test/kotlin directory in the project root:
Add module include inside settings.gradle.kts file:
Create konsistTest/src/test/kotlin directory in the project root:
Add module include inside settings.gradle.kts file:
Gradle's default behavior assumes that a module's code is up-to-date if the module itself hasn't been modified. This can lead to issues when Konsist tests are placed in a separate module. In such cases, Gradle may skip these tests, believing they're unnecessary.
However, this approach doesn't align well with Konsist's functionality. Konsist analyzes the entire codebase, not just individual modules. As a result, when Gradle skips Konsist tests based on its module-level change detection, it fails to account for potential changes in other modules that Konsist would typically examine.
There are few solutions to this problem.
An alternative solution for this problem is to define konsistTest module as always being out of date:
To avoid manually passing --rerun-tasks flag each time a custom konsistCheck task can be added to the root build config file:
Add to root build.gradle.kts:
Add to root build.gradle:
After adding konsistCheck task run ./gradlew konsistCheck to execute all Konsist tests.
val foo: FooKonsist
.scopeFromProject()
.properties()
.types
.assertTrue { koTypeDeclaration ->
val koClass = koTypeDeclaration as KoClassDeclaration
koClass.hasAllAnnotations {
it.representsTypeOf<String>()
}
}Konsist
.scopeFromProject()
.properties()
.types
.assertTrue { koTypeDeclaration ->
koTypeDeclaration
.asClassDeclaration
?.hasAllAnnotations {
it.representsTypeOf<String>()
}
}internal class Fooval foo: Foo? = nullscope
.properties()
.types
.assertTrue {
it.asClassDeclaration?.hasInternalModifier
}internal interface Fooval foo: Foo? = nullscope
.properties()
.types
.assertTrue {
it.asInterfaceDeclaration?.hasInternalModifier
}internal object Fooval foo: Foo? = nullscope
.properties()
.types
.assertTrue {
it.asObjectDeclaration?.hasInternalModifier
}internal object Footypealias MyFoo = Foo
val foo: MyFoo? = nullscope
.properties()
.types
.assertTrue {
it
.sourceTypeAlias
.type
.sourceInterface
.hasInternalModifier
}internal object Fooimport com.app.Foo as MyFoo
val foo: MyFoo? = nullscope
.properties()
.types
.assertTrue {
it
.asTypeAliasDeclaration
.type
.asInterfaceDeclaration
.hasInternalModifier
}// Kotlin internal source code for Stringval foo: String? = nullscope
.properties()
.types
.assertTrue {
it.asKotlinTypeDeclaration.name == "String"
}// Kotlin internal source codeval foo: () -> Unit? = nullscope
.properties()
.types
.assertTrue {
it
.sourceFunctionType
.parameterTypes
.isEmpty()
}class MyViewModel: ViewModelcom.android.library plugin in the konsistTest/scr/test/kotlin/build.gradle file.Refresh/Sync the Gradle Project in IDE.
❌
✅
✅
✅
✅
✅
✅
✅




// build.gradle.kts (root)
plugins {
`jvm-test-suite`
}
testing {
suites {
register("konsistTest", JvmTestSuite::class) {
dependencies {
// Add 'main' source set dependency
implementation(project())
// Add Konsist dependency
implementation("com.lemonappdev:konsist:0.13.0")
}
}
}
}
// Optional : Remove Konsist tests from the 'check' task if it exists
tasks.matching { it.name == "check" }.configureEach {
setDependsOn(dependsOn.filter { it.toString() != "konsistTest" })
}// build.gradle (root)
plugins {
id 'jvm-test-suite'
}
testing {
suites {
test {
useJUnitJupiter()
}
konsistTest(JvmTestSuite) {
dependencies {
// Add 'main' source set dependency
implementation project()
// Add Konsist dependency
implementation "com.lemonappdev:konsist:0.13.0"
}
targets {
all {
testTask.configure {
shouldRunAfter(test)
}
}
}
}
}
}
// Optional: Remove Konsist tests from the 'check' task if it exists
tasks.matching { it.name == "check" }.configureEach { task ->
task.setDependsOn(task.getDependsOn().findAll { it.toString() != "konsistTest" })
}# app/pom.xml
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>add-konsist-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>${project.basedir}/src/konsistTest/kotlin</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>./gradlew app:konsistTestmvn test// settings.gradle.kts
include(":konsistTest")// settings.gradle
include ':konsistTest'// konsistTest/build.gradle.kts
tasks.withType<Test> {
outputs.upToDateWhen { false }
}// konsistTest/build.gradle
tasks.withType(Test) {
outputs.upToDateWhen { false }
}tasks.register("konsistCheck") {
group = "verification"
description = "Runs Konsist static code analysis"
doLast {
val output = ByteArrayOutputStream()
val result = project.exec {
commandLine("./gradlew", "konsistTest:test", "--rerun-tasks")
standardOutput = output
errorOutput = output
isIgnoreExitValue = true
}
println(output.toString())
if (result.exitValue != 0) {
throw GradleException("Konsist tests failed")
}
}
}tasks.register("konsistCheck") {
group = "verification"
description = "Runs Konsist static code analysis"
doLast {
def output = new ByteArrayOutputStream()
def result = project . exec {
commandLine './gradlew', 'konsistTest:test', '--rerun-tasks'
standardOutput = output
errorOutput = output
ignoreExitValue = true
}
println output . toString ()
if (result.exitValue != 0) {
throw new GradleException ("Konsist tests failed")
}
}
}Spread the word: You can help to spread the word about the Konsist by talking about it with fellow developers. You can also write a short post or a full-fledged article. Make sure to let us know at #konsist channel if you do so.
Make sure all checks are passing before marking PR as Ready for review.
./gradlew detektCheck - check the code using Detekt
./gradlew detektApply - check and fix code using Detekt (if possible)
Tests
./gradlew lib:test - run JUnit tests
./gradlew lib:apiTest - run API tests
./gradlew lib:integrationTest - run integrations tests
./gradlew lib:konsistTest - run Konsist tests to test Konsist codebase 🤯😉
.kttxtkonsistTest - tests Konsist codebase consistency using konsist library
snippets - contains Kotlin code snippets, written as methods (tests without @Test annotation), so the tests are not executed. These snippets are used to generate documentation. The update-snippets.py script generates PR to update the snippets page





Access the Kotlin files using Konsist API
Scope represents a set of Kotlin files to be further queried, filtered (Declaration Filtering), and verified (Declaration Assertion).
Every scope contains a set of KoFile instances. Every KoFile instance contains the declarations (see Declaration) representing code entities present in the file e.g.:
The scope can be created for an entire project, module, package, and Kotlin file.
The scope is dynamically built based on the Kotlin files present in the project, enabling it to adapt seamlessly as the project evolves. For instance, when the scope is set to encapsulate a specific module, any additional file introduced to that module will be automatically incorporated into the scope. This ensures that the scope consistently offers thorough and current coverage.
To execute Konsist tests, the Konsist dependency must be integrated into a module. Yet, by integrating Konsist into a single module (e.g. app module), Konsist can still access the entire project. The specific files evaluated are determined by the evolving scope that's been defined.
Various methods can be used to obtain instances of the scope. This allows the definition of more granular Konsist tests e.g. tests covering only certain modules, source sets, packages, or folders.
The widest scope is the scope containing all Kotlin files present inside the project:
To print a list of files within koScope use the koScope.print() method:
The scopeFromProduction method allows the creation of a scope containing only a production code (equivalent to Konsist.scopeFromProject() - Konsist.scopeFromTest()):
Contains:
The scopeFromTest method allows the creation of a scope containing only a test code:
Contains:
The scopeFromModule method allows the creation of more granular scopes based on the module name e.g. creating a scope containing all Kotlin files present in the app module:
Contains:
This approach may be helpful when refactoring existing project modules by module.
A nested module is a module that exists within another module.
The nested modules the feature is not complete. The community is reporting that this feature works, however, we still have to take a closer look, review expectations, and add tests. Consider this feature as experimental for now.
Consider this feature module existing inside app module:
To narrow the scope to feature module use:
The scopeFromSourceSet method argument allows the creation of more granular scopes based on the source set name e.g. create a scope containing all Kotlin files present in the test source set:
Contains:
To retrieve scope by using both module and source set use the scopeFromProject method with moduleName and sourceSetName arguments:
Contains:
The sourceFromPackage method allows the creation of a scope containing code present in a given package e.g. com.usecase package:
Contains:
The scopeFromDirectory method allows the creation of a scope containing code present in a given project folder e.g. domain directory:
Contains:
It is also possible to create scope from one or more file paths:
We have added a new way of creating the scope from a list of files. This can help with certain development workflows e.g. runing Konsist Tests only on files modified in a given PR:
For even more granular control you can use the KoScope.slice method to retrieve a scope containing a subset of files from the given scope:
The KoScope can be printed to display a list of all files present in the scope. Here is an example:
To reuse scope across the test class define the scope in the companion object and access it from multiple tests:
To reuse scope across the multiple test classes define the scope in the file and access it from multiple test classes:
Here is the file structure representing the above snippet:
Konsist scope supports , so scopes can be further combined together to create the desired scope, tailored to project needs. In this example scopes from myFeature1 module and myFeature2 module are combined together:
Scope subtraction is also supported, so it is possible for example to exclude a part of a given module. Here scope is created from myFeature module and then the ..data.. package is excluded:
To print all files within the scope use the print() method:
To access specific declaration types such as interfaces, classes, constructors, functions, etc. utilize the .
./gradlew publishToMavenLocal -Pkonsist.releaseTarget=localMac: /Users/<user_name>/.m2/repository/com/lemonappdev/konsist
Windows: C:\Users\<User_Name>\.m2\repository\com\lemonappdev\konsist
Linux: /home/<User_Name>/.m2/repository/com/lemonappdev/konsistrepositories {
mavenLocal()
}<repositories>
<repository>
<id>local</id>
<url>file://${user.home}/.m2/repository</url>
</repository>
</repositories>Konsist.scopeFromProject() // All Kotlin files present in the projectKonsist
.scopeFromProject()
.print()Konsist.scopeFromProduction()project/
├─ app/
│ ├─ main/ <--- scope contains all production code files
│ │ ├─ App.kt
│ ├─ test/
│ │ ├─ AppTest.kt
├─ core/
│ ├─ main/ <--- scope contains all production code files
│ │ ├─ Core.kt
│ ├─ test/
│ │ ├─ CoreTest.ktKonsist.scopeFromTest()project/
├─ app/
│ ├─ main/
│ │ ├─ App.kt
│ ├─ test/ <--- scope contains all test code files
│ │ ├─ AppTest.kt
├─ core/
│ ├─ main/
│ │ ├─ Core.kt
│ ├─ test/ <--- scope contains all test code files
│ │ ├─ CoreTest.ktKonsist.scopeFromModule("app")project/
├─ app/ <--- scope contains all files from the 'app' module
│ ├─ main/
│ │ ├─ App.kt
│ ├─ test/
│ │ ├─ AppTest.kt
├─ core/
│ ├─ main/
│ │ ├─ Core.kt
│ ├─ test/
│ │ ├─ CoreTest.ktval refactoredModule1Scope = Konsist.scopeFromModule("refactoredModule1")
val refactoredModule1Scope = Konsist.scopeFromModule("refactoredModule2")
val scope = refactoredModule1Scope + refactoredModule1Scop2
scope
.classes()
...
.assertTrue { /*..*/ }project/
├─ app/ <--- scope contains all files from the 'app' module
│ ├─ feature/
│ │ ├─ Feature.ktKonsist.scopeFromModule("app/feature")Konsist.scopeFromSourceSet("test")project/
├─ app/
│ ├─ main/
│ │ ├─ App.kt
│ ├─ test/ <--- scope contains all files the 'test' directory
│ │ ├─ AppTest.kt
├─ core/
│ ├─ main/
│ │ ├─ Core.kt
│ ├─ test/ <--- scope contains all files the 'test' directory
│ │ ├─ CoreTest.ktKonsist.scopeFromProject(moduleName = "app", sourceSetName = "test)
project/
├─ app/
│ ├─ main/
│ │ ├─ App.kt
│ ├─ test/ <--- scope contains all files the 'test' directory
│ │ ├─ AppTest.kt
├─ core/
│ ├─ main/
│ │ ├─ Core.kt
│ ├─ test/
│ │ ├─ CoreTest.ktKonsist.sourceFromPackage("com.usecase..")project/
├─ app/
│ ├─ main/
│ │ ├─ com/
│ │ │ ├─ usecase/
│ │ │ │ ├─ UseCase.kt <--- scope contains files present from 'com.usecase' package kon
│ ├─ test/
│ │ ├─ com/
│ │ │ ├─ usecase/
│ │ │ │ ├─ UseCaseTest.kt <--- scope contains files present from 'com.usecase' packageval myScope = Konsist.scopeFromDirectory("app/domain")project/
├─ app/
│ ├─ main/
│ │ ├─ com/
│ │ │ ├─ domain/ <--- scope contains files present in 'domain' folderval myScope = Konsist.scopeFromFile("app/main/domain/UseCase.kt")val filePaths = listOf("/domain/UseCase1.kt", "/domain/UseCase2.kt")
val myScope = Konsist.scopeFromFile(filePaths)// scope containing all files in the 'test' folder
koScope.slice { it.relativePath.contains("/test/") }
// scope containing all files in 'com.domain.usecase' package
koScope.slice { it.hasImport("com.domain.usecase") }
// scope containing all files in 'usecase' package and its sub-packages
koScope.slice { it.hasImport("usecase..") }// Test.kt
class DataTest {
@Test
fun `test 1`() {
classesScope
.assertTrue { // .. }
}
fun `test 2`() {
classesScope
.assertTrue { // .. }
}
companion object {
// Create a new KoScope once for all tests
private val classesScope = Konsist
.scopeFromProject()
.classes()
}
}// Scope.kt is "test" source set
val projectScope = Konsist.scopeFromProject() // Create a new KoScope
// AppTest.kt
class AppKonsistTest {
@Test
fun `test 1`() {
projectScope
.objects()
.assertTrue { // .. }
}
}
// DataTest.kt
class CoreKonsistTest {
@Test
fun `test 1`() {
projectScope
.classes()
.assertTrue { // .. }
}
fun `test 2`() {
projectScope
.interfaces()
.assertTrue { // .. }
}
}project/
├─ app/
│ ├─ test/
│ │ ├─ app
│ │ ├─ AppKonsistTest.kt
│ │ ├─ core
│ │ ├─ CoreKonsistTest.kt
│ │ ├─ Scope.kt <--- Instance of the KoScope used in both DataTest and AppTest classes.val featureModule1Scope = Konsist.scopeFromModule("myFeature1")
val featureModule2Scope = Konsist.scopeFromModule("myFeature2")
val refactoredModules = featureModule1Scope + featureModule2Scope
refactoredModules
.classes()
...
.assertTrue { ... }val moduleScope = Konsist.scopeFromModule("myFeature")
val dataLayerScope = Konsist.scopeFromPackage("..data..")
val moduleSubsetScope = moduleScope - dataLayerScope
moduleSubsetScope
.classes()
...
.assertTrue { ... }koScope.print()