Contents

Kotest: The Kotlin Testing Framework You Will Love

Kotest: The Kotlin Testing Framework You Will Love webp image

You might have already had a chance to read my previous blog post about useful Kotlin libraries and frameworks for backend development. If you have, then you already know that Kotest is a comprehensive, convenient, and modular testing framework written in Kotlin.

If you haven’t read it, though, I highly recommend you do so because it provides some helpful background and a quick overview, not only of Kotest but also of other interesting Kotlin libraries. But of course, it’s totally optional, as in this article, I will dive much deeper into Kotest’s features. There are going to be a lot of practical examples and code snippets here, so fasten your seatbelts and get ready!

Kotest modules

Kotest is a modular framework that currently consists of three main modules: Test Framework, Assertions Library, and Property Testing. You can use all the modules together if you want, but nothing will stop you from picking just the ones you need and mixing them with other testing frameworks or libraries.

For example, you could use only Kotest Assertions in your JUnit tests. Or you could write your tests using the Kotest core Test Framework and employ AssertJ for assertions. You may also want to use Kotest with Spring Boot Tests, and that will work seamlessly as well.

Kotest Test Framework

The Test Framework is the core module of Kotest. Kotest tests are versatile and can be executed on JVM, Javascript, and Native environments, making it a multi-platform testing framework. In this blog post however I’m going to focus entirely on JVM.

To include it in your Gradle project, all you need to do is define the following dependency and tell Gradle to use the JUnit platform for test execution:

dependencies {
   …
   testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
   …
}

tasks.test {
   useJUnitPlatform()
}

Testing styles

If I were to pick one keyword that best describes the Kotest framework, it would definitely be flexibility. This is mainly because of the way Kotest allows you to define test cases. It essentially offers ten different testing styles!

Thanks to this, every developer who starts using Kotest should feel at home, regardless of their background technology, as each testing style is inspired by an existing programming language or testing framework.

As an example, here’s how you can implement a test case using the default testing style, which is called FunSpec:

class MyFunSpec: FunSpec({
   test("2 plus 2 should be 4") {
       (2 + 2) shouldBe 4
   }
})

But if you’re more into BDD, you can also implement the same test case differently by using BehaviorSpec:

class MyBehaviorSpec: BehaviorSpec({
   Given("Number 2") {
       val num1 = 2
       And("another number 2") {
           val num2 = 2
           When ("adding those numbers") {
               val result = num1 + num2
               Then("the result should be 4") {
                   result shouldBe 4
               }
           }
       }
   }
})

These are just two of the ten available testing styles. If you’d like to learn about all of them, don’t hesitate to check out the official Kotest documentation, which lists all the testing styles.

Kotest vs JUnit

I’d like to draw your attention to one more thing. Look closely at how test cases are defined in Kotest. Unlike JUnit, where test cases are function definitions, in Kotest, they are function calls inside the init block of a Spec class. It boils down to the fact that Kotest offers us a DSL for writing tests. This seemingly small detail provides a powerful capability: the ability to generate test cases dynamically at runtime.

For example, we can do something like this:

class DynamicSpec : FunSpec({
   for (i in 1..10) {
       test("Test nr $i") {
           i shouldBeGreaterThan 0
       }
   }
})

In the example above, 10 test cases will be generated, each with a dynamically generated name. While this specific example may not be particularly useful, it clearly illustrates the concept.

But we can take this even further. Suppose our project contains a directory src/test/resources/test_definitions, with .txt files with content structured like that:

This is first test
2,2
4

We can then define a test suite that dynamically generates test cases based on these files:

class DynamicSpec : FunSpec({
   File("src/test/resources/test_definitions").walkTopDown()
       .filter { it.name.endsWith(".txt") }
       .forEach { testFile ->
           val lines = testFile.readLines(Charsets.UTF_8)
           val testName = lines[0]
           val numbersToBeAdded = lines[1]
               .split(",")
               .map(String::toInt)
           val expectedResult = lines[2].toInt()

           // This will generate separate test case for each file in test_definitions directory
           test(testName) {
               numbersToBeAdded.reduce(Int::plus) shouldBe expectedResult
           }
       }
})

This way we just defined a dynamic test suite where test cases are generated based on external text files with custom content.

This is, of course, just one example, but you can leverage this feature in many creative ways. How cool is that?!

Lifecycle Hooks and Extensions

Apart from different styles, Kotest also offers a flexible lifecycle hooks and extensions mechanism. It provides all the standard hooks we know from other testing frameworks, such as beforeSpec, afterSpec, beforeTest, afterTest, etc. However, we can also easily implement and plug in our custom extensions and hooks.

Conditional Evaluation

Kotest provides powerful tools for running tests conditionally. There are many options, ranging from simply disabling specific test cases to running only those that fulfill complex conditions evaluated at runtime. The example below should give you an idea of what you can do in this area:

test("This will never run").config(enabled = false) {
   // test something here
}

test("This will only run on Linux").config(enabled = SystemUtils.IS_OS_LINUX) {
   // test something here
}

fun isNight() = now().isAfter(LocalTime.of(20, 0)) || now().isBefore(LocalTime.of(8, 0))

test("This will run only nightly").config(enabledIf = { isNight() }) {
   // test something here
}

Test Organization and Filtering

Kotest provides several ways to organize and filter your tests, making it easier to manage large test suites and focus on specific areas of your codebase.

One of the key features for organizing tests is the describe function of DescribeSpec, which allows you to group related tests together. This can help you structure your test code in a way that reflects the functionality being tested, making it more readable and maintainable.

Here’s an example of using the describe function to group tests:

class MyDescribeSpec : DescribeSpec({
   describe("Arithmetic operations") { 
      it("should add numbers correctly") { 
         (2 + 2) shouldBe 4 
      }

      it("should subtract numbers correctly") {
         (4 - 2) shouldBe 2
      }
   }
})

In addition to grouping tests, Kotest allows you to use tags to label and filter your tests. Tags can be used to categorize tests based on criteria such as functionality, priority, or environment. You can then run or exclude tests based on these tags, which is particularly useful for large projects with extensive test suites.

Here’s an example of tagging tests:

class MyTaggedSpec : FunSpec({
    test("important test").config(tags = setOf(MyTags.Important)) {
        // test code
    }

    test("optional test").config(tags = setOf(MyTags.Optional)) {
        // test code
    }
})

object MyTags {
    val Important = Tag("Important") 
    val Optional = Tag("Optional")
}

To run only the important tests, you can use the following command:

gradle test --tests * --include-tag Important 

By using the describe function and tags, you can effectively organize and filter your tests, making it easier to manage and execute them as needed.

Coroutines Support

As you might expect from a framework written in and targeted at Kotlin, coroutine support is built into Kotest. We can run our test cases within a coroutineTestScope, enabled either inside the config function for a specific test case or globally for all test cases in our spec. This runs the test using a TestDispatcher built into the kotlinx.coroutines library, which in turn allows us to manipulate virtual time inside tests, skipping delays, etc.

Kotlin coroutines are such a wide topic that I’m not going to elaborate on them more in this blog post. But please keep an eye out for our blog for more articles about coroutines, which should appear there soon.

Data-Driven Testing

You might be familiar with parameterized tests from other testing frameworks (such as JUnit 5’s @ParameterizedTest or the where keyword in Spock). Data-driven tests in Kotest follow the same idea. The philosophy behind this approach is to:

  • implement a test case that accepts parameters,
  • prepare various sets of those parameters,
  • run the test case for all parameter sets, expecting it to pass for each iteration.

To use data-driven testing, you’ll first need to include the following dependency in your Gradle build file:

testImplementation("io.kotest:kotest-framework-datatest:$kotestVersion")

The way you can prepare such test cases using Kotest is as follows:

context("numbers should add up to 10") {
   withData(
       Pair(5, 5),
       Pair(3, 7),
       Pair(1, 9)
   ) { (num1, num2) ->
       num1 + num2 shouldBe 10
   }
}

context("numbers should add up to given result") {
   withData(
       nameFn = { "${it.first } + ${it.second} = ${it.third}" }, // generate descriptive names for all cases
       Triple(1, 2, 3),
       Triple(10, 5, 15),
       Triple(-5, 5, 0),
   ) { (num1, num2, result) ->
       num1 + num2 shouldBe result
   }
}

That’s it for the overview of the Kotest core module. Now, let’s see what other cool features the remaining two modules offer.

Assertions Library

The crucial aspect of every test is its assertions. I suppose you’d be astonished if someone told you they had a project with 100% code coverage and all tests passing all the time. However, this astonishment would turn to dust as soon as they mentioned that their tests had no assertions.

Fortunately, Kotest comes with a comprehensive library of various assertions for different data types. Additionally, thanks to the syntactic sugar built into Kotlin, Kotest assertions are also extremely expressive, convenient, and easy to read.

To use the assertions library in your project, you’ll need to add the appropriate dependency to your build file:

testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")

Matchers

The main backbone of the Kotest assertions module is matchers. You can think of matchers as functions that verify whether a value matches the expected result. Kotest provides a variety of built-in matchers. Some of them are as follows:

test("various matchers") {
   (2 + 2) shouldBe 4
   "text" shouldContain "x"
   "non null text" shouldNotBe null
   10 shouldBeGreaterThan 8
   15L should beInstanceOf<Number>()
   20 should beOfType<Int>()
   mapOf("key" to "value") shouldContainValue "value"
   listOf(1, 2, 3).shouldNotBeEmpty()
   "{\"someProp\":\"value\"}" shouldContainJsonKey "someProp"
   LocalDate.parse("2025-02-17") shouldBeAfter LocalDate.parse("2025-01-22")
}

Note that, among other matchers in the example above, I also used shouldContainJsonKey. This one is special, as it’s one of the JSON matchers. To be able to use this or any other JSON matchers, you’ll need to add the additional dependency to your build file:

testImplementation("io.kotest:kotest-assertions-json:$kotestVersion")

Inspectors

Kotest also provides a special API for verifying elements of collections. It’s called Inspectors and allows you to match the contents of a collection according to your needs. Here’s a sample usage of inspectors:

test("Collection inspectors") {
   val numbers = listOf(2, 4, 6, 8, 10)

   numbers.forAll { it.shouldBeEven() }
   numbers.forNone { it.shouldBeOdd() }
   numbers.forSome { it shouldBeLessThan 5 }
   numbers.forExactly(2) { it shouldBeGreaterThan 7 }
   numbers.forAtMostOne { it shouldBeInRange 0..3 }
}

Custom matchers

If you still lack some matchers in the standard library, you might want to implement your own. Remember that you can fully utilize Kotlin extension functions and infix functions for that!

It’s as simple as defining a function (or functions) that act as matchers:

fun aGreetingTo(name: String) = Matcher<String> { value ->
   MatcherResult(
       value.matches(Regex("Hello $name!")),
       { "Message '$value' should be a greeting to $name!" },
       { "Message '$value' should not be a greeting to $name!" }
   )
}

infix fun String.shouldBeAGreetingTo(name: String): String { // optional extension version of the matcher
   this shouldBe aGreetingTo(name)
   return this
}

infix fun String.shouldNotBeAGreetingTo(name: String): String { // optional negated extension version of the matcher
   this shouldNotBe aGreetingTo(name)
   return this
}

Using those matchers in tests would then look like this:

test("should message be a greeting to John") {
   "Hello John!" shouldBe aGreetingTo("John") // custom matcher
   "Hello Mary!" shouldNotBe aGreetingTo("John") // negated custom matcher
   "Hello John!" shouldBeAGreetingTo("John") // extension custom matcher
   "Goodbye John!" shouldNotBeAGreetingTo("John") // negated extension custom matcher
}

Composed matchers

Additionally, Kotest allows us to join various matchers (both built-in and custom) into composed matchers. Thanks to this, we can group our conditions into more complex ones, checking either if all conditions are met or if any of them is fulfilled.

val containAllABC = Matcher.all(
   contain("A"), contain("B"), contain("C")
)

val containAnyOfABC = Matcher.any(
   contain("A"), contain("B"), contain("C")
)

test("composed matchers") {
   "ABC" should containAllABC
   "A" should containAnyOfABC
}

Soft assertions

Another small but useful feature of Kotest is the ability to define soft assertions. By default, Kotest uses the fail-fast strategy when running test cases, which means it immediately stops test execution on the first failing assertion. This little feature, however, allows you to run all assertions regardless of whether any of them fail. Of course, the test case will still fail if any of the assertions fail; the only difference is that all assertions will be verified anyway.

test("soft assertions") {
   val text = "Some text"

   assertSoftly {
       text shouldContain "no such string"
       text shouldStartWith "S"
       text shouldHaveLength 100
   }
}

Non-deterministic testing

When writing unit tests, we’d probably prefer to deal with code that behaves in a fully predictable way and executes immediately. But unfortunately, that’s not always the case. There are situations where we need to test asynchronous operations, for which the only thing we know is that they should eventually finish. How do we deal with that? With Kotest’s non-deterministic testing, it’s a simple task.

test("should eventually store order") {
   val order = Order("order-1", 99.99)

   someAsynchronousDb.save(order)

   eventually(10.seconds) { // verifies that something eventually happens
       someAsynchronousDb.getById(order.id) shouldBe order
   }
}

test("should keep data in cache according to TTL") {
   val cacheTTL = 20.seconds

   cache.put("entry-1", "some cached value")

   continually(cacheTTL) { // verifies that some condition is true for the given period of time
       cache["entry-1"] shouldBe "some cached value"
   }
}

That’s everything I wanted to share with you about the Kotest Assertions Library. But there’s still some interesting content I’d like to show you: the last module of Kotest — Property Testing.

Property Testing

Unit tests that developers usually write are based on examples — developers feed some example input data into a test subject and expect specific results. This approach is called example-based testing. This approach is acceptable in most cases, but it has its drawbacks. For example, it’s easy to miss some edge cases in our examples. It’s also sometimes cumbersome to manually define more complex dummy data for tests. But this is not the only way we can unit test our code — there is also an alternative approach, called property-based testing, and Kotest supports that as well.

Property-based testing involves automatically generating random test data, including various edge cases and regular cases, running the test multiple times, and feeding this generated data to it.

To benefit from property testing in your project, you’ll need the following dependency in your Gradle build file:

testImplementation("io.kotest:kotest-property:$kotestVersion")

Property test functions

To give you an idea of how property tests are defined in Kotest, I’m going to show you some property tests for the Fibonacci function:

class MyPropertyTestSpec: StringSpec({
   "test fibonacci for n > 1 using forAll and arbitrary generator" { // 1
       forAll(Arb.int(2..10)) { n ->
           fibonacci(n) == fibonacci(n - 1) + fibonacci(n - 2)
       }
   }

   "test fibonacci for n > 1 using checkAll" { // 2
       checkAll(100, Arb.int(2..10)) { n ->
           fibonacci(n) shouldBeEqual fibonacci(n - 1) + fibonacci(n - 2)
       }
   }

   "test fibonacci for n > 1 using forAll and exhaustive generator" { // 3
       forAll(Exhaustive.ints(2..10)) { n ->
           fibonacci(n) == fibonacci(n - 1) + fibonacci(n - 2)
       }
   }

   "test fibonacci for n < 0" { // 4
       checkAll(Arb.int(max = -1)) { n ->
           shouldThrow<IllegalArgumentException> { fibonacci(n) }
       }
   }
})

There are four separate test cases defined here, each showing different flavors of Kotest. Let me explain them one by one:

  1. This is a simple property-based test case. The crucial thing here is the forAll function, which invokes its body multiple times (1000 times by default). Arb.int(2..10) is a generator that tells Kotest what values it should pass to the test. In this case, n will always be a random number between 2 and 10. If we don't specify a generator here, Kotest would generate numbers from Int.MIN_VALUE to Int.MAX_VALUE by default. One more thing about the forAll function is that it accepts a lambda of type (a, …, n) -> Boolean, and to pass the test, all the calls to the lambda need to return true.
  2. This test case is very similar to the previous one, with two main differences. The first one is that we’re using the checkAll function instead of forAll to define our property-based test. CheckAll accepts a lambda of type (a, …, n) -> Unit, so here we can use assertions to verify our conditions - no need to return a boolean as in the forAll function. The second difference is that we explicitly specify the number of iterations for that test, which is 100 in our case. And, of course, the body is a bit different, because now we’re verifying the function result using assertions and not returning boolean values.
  3. Looking at the two test cases above, you might ask yourself: “Why do we need 1000 or even 100 test iterations, with randomly generated numbers, to just test the deterministic Fibonacci function with values from 2 to 10?”. And you’re right - we have a better alternative for this particular case, which is shown in test case number 3. Mind that instead of using an Arbitrary Generator for our n parameter, we’re using Exhaustive.ints(2..10) here. This means that for this test case, Kotest will generate all the values from the given range and run the test case only once for each value. So instead of 1000 or 100 test runs, we have 9 iterations, covering every value from range 2-10.
  4. The fourth test case doesn’t show many new Kotest features compared to the previous ones. It basically shows an alternative variant of the Arbitrary Generator for Ints (a half-open set of Int values, from Int.MIN_VALUE to -1). It also shows that we can test for exceptions in a property-based test case.

Generator operations

In the previous section, you’ve seen some generators. You already know that we can either use Arb or Exhaustive generators to produce data for our property-based tests. Soon you will also learn (spoiler alert!) about Custom Generators.

But in this section, you will see that even built-in generators provide some useful operations that allow us to adapt them to our needs - from simply fetching the next item from a generator to complex filtering and mapping of streams of values that generators produce. Let’s take a look at some examples below:

val someRandomString: String = Arb.string(1..10).next()
val oddNumbers: Arb<Int> = Arb.int().filter { it % 2 == 1 }
val prefixedStrings: Arb<String> = Arb.string(1..5).map { "PREFIX-$it" }
val suffixedStrings: Arb<String> = Arb.string(1..5).map { "$it-SUFFIX" }
val prefixedAndSuffixed: Arb<String> = prefixedStrings.merge(suffixedStrings)

Type declarations are redundant here. I placed them only to show you that most of the generator operations also return generators, so we can use them in property tests as usual or manipulate and convert them further. The next operation is unique here because, as you can see, it fetches the actual value from a generator.

Assumptions

Sometimes, there might be a situation where not all combinations of input parameters make sense in our test case. This is where assumptions come into play. This is the way to go if we want to skip particular iterations of our test case based on the parameters. Assumptions can be either defined as a conditional expression or even using assertions. Let’s see some examples:

"test with assumptions conditional" {
   checkAll<String, String> { a, b ->
       withAssumptions(a != b) {
           // test here
       }
   }
}

"test assume conditional" {
   checkAll<String, String> { a, b ->
       assume(a != b)
       // test here
   }
}

"test with assumptions assertions" {
   checkAll<String, String> { a, b ->
       withAssumptions({
           a shouldNotBe b
       }) {
           // test here
       }
   }
}

"test assume assertions" {
   checkAll<String, String> { a, b ->
       assume {
           a shouldNotBe b
       }
       // test here
   }
}

Custom generators

As I’ve already indicated, we’re not limited to predefined generators for our property-based tests. Kotest offers the capability to define our own custom generators. I think this is particularly useful if we want to generate instances of our classes.

We can implement both arbitrary and exhaustive generators. To see how to do that, refer to the code snippet below:

class Address(val city: String, val street: String, val number: Int, val zipCode: String)

val citiesGenerator = listOf("New York", "London", "Paris", "Warsaw", "Poznan").exhaustive()

val addressGenerator = arbitrary {
   Address(
       city = citiesGenerator.toArb().bind(), // cannot bind to exhaustive generator directly
       street = Arb.string(5..20).bind(),
       number = Arb.positiveInt(100).bind(),
       zipCode = Arb.stringPattern("\\d{2}-\\d{3}").bind() // can also generate strings based on regex
   )
}

This way, we can generate our Address instances the same way we generate Ints or Strings, and use them in property-based test cases. Also, nothing stops us from using data generated this way in regular test cases. In case we don’t want to manually define our test data, we can always generate some.

That’s all for the Property Testing module of Kotest. I hope you found the information useful and interesting.

Best Practices

Here are some best practices to keep in mind when using Kotest:

  • Use descriptive names for your tests and test functions: Clear and descriptive names make it easier to understand what each test is verifying, which is crucial for maintaining and debugging your test suite.
  • Use the describe function to group related tests together: Grouping related tests helps organize your test code and makes it more readable.
  • Use tags to label and filter your tests: Tags allow you to categorize and selectively run tests, which is especially useful for large projects.
  • Use property-based testing and data-driven testing to write more effective tests: These techniques help ensure that your tests cover a wide range of inputs and scenarios, improving the robustness of your test suite.

Combining with other technologies

As a ‘bonus’, I’d like to share with you some ideas on how Kotest can be used with popular backend JVM technologies.

Spring Boot

Let’s start with one of the most popular JVM web frameworks, which undoubtedly is Spring Boot. Kotest happens to offer a dedicated, official Spring extension. Assuming you have a Spring Boot Kotlin project, adding Kotest to it is as simple as including the following dependencies in your build file (Gradle in my case, but Maven will work as well):

testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testImplementation("io.kotest.extensions:kotest-extensions-spring:$kotestSpringExtensionVersion")

After you do so, you can write your tests using Spring Boot Test and Kotest combined:

@ContextConfiguration(classes = [MyConfiguration::class]) // configuration classes to use in tests
@ActiveProfiles("test") // active Spring profiles
class SpringBootKotestSampleSpec(
   orderService: OrderService // injected dependency
) : FunSpec() {
   override fun extensions() = listOf(SpringExtension) // adding Spring Extension to the test suite

   init {
       test("should find created order") {
           println("Test context is ${testContextManager().testContext}") // testContextManager is available thanks to SpringExtension

           // given
           val orderId = orderService.placeOrder() // using injected service

           // when
           val order = orderService.findOrderById(orderId)

           // then
           order.orderId shouldBe orderId
       }
   }
}

Ktor

If you had a chance to read my previous blog post about useful Kotlin frameworks and libraries for backend development, you might recall a web framework called Ktor. It’s a lightweight, fully-fledged framework written in Kotlin that supports many plugins and extensions for various widely used tools, like different DBs, message brokers, etc. It is also compatible with Kotest, thanks to the official Kotest extension for Ktor.

To be able to use the Kotest extension in your Ktor project, it’s enough to add the following dependencies to your Gradle (or Maven) build file:

testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testImplementation("io.kotest.extensions:kotest-assertions-ktor:$kotestKtorVersion")

Once you have that, you can use Kotest's dedicated assertions for Ktor HTTP responses:

test("test greeting endpoint") {
   testApplication {
       application {
           module()
       }

       val response = client.get("/greetings/John")

       // assertions below are provided by Ktor Extension
       response.shouldHaveStatus(HttpStatusCode.OK)
       response.shouldNotHaveContentEncoding("gzip")
       response.shouldHaveHeader("Content-Type", "application/json; charset=UTF-8")
   }
}

Kotest in Java project

Up until now, I’ve been showing you the various ways to incorporate Kotest into your Kotlin projects using different frameworks. But what if you have a Java project and, for some reason, you cannot (or don’t want to) switch entirely to Kotlin? Well, in such a case, you can still benefit from Kotest, and I highly encourage you to do so. Both Kotlin and Java are JVM languages, and they can cooperate with each other. So, nothing stops you from writing your production code in Java but using Kotlin (and Kotest) for your unit tests. From my experience, such an approach is an excellent and safe way of learning a new language and actually using it in a real project. I know people often do this with Spock, which is a Groovy testing framework, combining it with Java. So why not give Kotest a try this way?

Ok, so let’s assume you’re convinced and have a Java project with a Gradle build file using Groovy syntax. How can you start using Kotest for unit testing in such a case? All you need to do is slightly modify your build.gradle file:

plugins {
   id 'java' // this is your Java plugin, that you already have
   ... // possibly some other Gradle plugins go here
   id 'org.jetbrains.kotlin.jvm' version '2.1.10' // You need to add a Kotlin plugin for Gradle
}

dependencies {
   ...
   testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") // core dependency for Kotest
   testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") // other optional dependencies for Kotest, like Kotest Assertions
   ...
}

test {
   useJUnitPlatform() // Kotest works on top of JUnit platform, so this stays unchanged
}

For Maven projects, things become a bit more complex, but still doable:

<properties>
   ...
   <kotlin.version>2.1.10</kotlin.version> <!-- You can add Kotlin version as property -->
   <kotest.version>5.9.1</kotest.version> <!-- And Kotest version -->
</properties>

<build>
   <plugins>
       <plugin>
           <!-- This is Kotlin Maven plugin that you need to include -->
           <groupId>org.jetbrains.kotlin</groupId>
           <artifactId>kotlin-maven-plugin</artifactId>
           <version>${kotlin.version}</version>
           <executions>
               <execution>
                   <!--
                   As we only want to use Kotlin in tests,
                   then just test-compile goal needs to be specified 
                   -->
                   <id>test-compile</id>
                   <goals>
                       <goal>test-compile</goal>
                   </goals>
                   <configuration>
                       <sourceDirs>
                        <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
                       </sourceDirs>
                   </configuration>
               </execution>
           </executions>
       </plugin>

       <plugin>
           <!--
           As we changed the compilation process by adding Kotlin compiler,
           we also need to do some changes to a default Maven compiler
           -->
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.5.1</version>
           <executions>
               <!-- Disable default compile tasks -->
               <execution>
                   <id>default-compile</id>
                   <phase>none</phase>
               </execution>
               <execution>
                   <id>default-testCompile</id>
                   <phase>none</phase>
               </execution>
               <!-- Define java-compile task which will only compile production code -->
               <execution>
                   <id>java-compile</id>
                    <phase>compile</phase>
                   <goals>
                       <goal>compile</goal>
                   </goals>
               </execution>
           </executions>
       </plugin>
   </plugins>
</build>

<dependencies>
   <!-- Here we add Kotest dependencies -->
   <dependency>
       <groupId>io.kotest</groupId>
       <artifactId>kotest-runner-junit5</artifactId>
 <version>${kotest.version}</version>
       <scope>test</scope>
   </dependency>
   <dependency>
       <groupId>io.kotest</groupId>
       <artifactId>kotest-assertions-core</artifactId>
       <version>${kotest.version}</version>
       <scope>test</scope>
   </dependency>
</dependencies>

And that’s all! You can now create regular Kotest test suites inside the /src/test/kotlin directory of your Java project.

Wrap up

This has been quite a long journey! My intention was to lead you through various features of Kotest - a testing framework written in and for Kotlin. We went through the core features of the Test Framework module. We learned various convenient matchers from the Assertions Library module. We familiarized ourselves with the property-based testing approach offered by the Property Testing module. Finally, we tried (and succeeded!) to combine Kotest with some other JVM frameworks and languages.

It was a long journey indeed, but it doesn’t need to end here. For you, it might be just the start of a really exciting journey of using Kotest in your projects and experimenting with it further on your own! I hope you enjoy it!

Reviewed by: Rafał Wokacz, Paweł Antoniuk

Blog Comments powered by Disqus.