Leoš Přikryl
@leos_prikryl

commity.cz, GDG Jihlava

Testing in Kotlin

Frameworks

  • JUnit (Java)
  • Spek
  • KotlinTest

Matchers & Assertions

  • JUnit (Java)
  • AssertJ (Java)
  • AssertK
  • Hamkrest
  • Kluent
  • Strikt
  • KotlinTest

Mocking

  • Mockito (Java)
  • Mockito-Kotlin
  • MockK


KotlinTest

  • Kotlin testing framework
  • inspired by ScalaTest
  • flexible
  • idiomatic Kotlin

Testing Styles

Testing Styles

  • StringSpec
  • FunSpec
  • AnnotationSpec
  • DescribeSpec
  • WordSpec
  • ShouldSpec
  • FeatureSpec
  • BehaviorSpec
  • FreeSpec
  • ExpectSpec

String Spec

class MyTests : StringSpec({
    "String.length should return size of string" {
      "hello".length shouldBe 5
    }
})

Fun Spec

class MyTests : FunSpec({
    test("String.length should return the length of the string") {
        "sammy".length shouldBe 5
        "".length shouldBe 0
    }
})

Annotation Spec

class AnnotationSpecExample : AnnotationSpec() {

  @BeforeEach
  fun beforeTest() {
    println("Before each test")
  }

  @Test
  fun test1() {
    1 shouldBe 1
  }

  @Test
  fun test2() {
    3 shouldBe 3
  }
}

Describe Spec

class MyTests : DescribeSpec({
    describe("score") {
        it("start as zero") {
            // test here
        }
        context("with a strike") {
            it("adds ten") {
                // test here
            }
            it("carries strike to the next frame") {
                // test here
            }
       }
   }
}

Matchers & Assertions

Basic Matchers

obj.shouldBe(other)

expr.shouldBeTrue()

expr.shouldBeFalse()

Exceptions

shouldThrow<InvalidArgumentException> {
    // a code that might throw an exception
}
val exception = shouldThrow<InvalidArgumentException> {
    // a code that might throw an exception
}
exception.message.shouldStartWith("Something went wrong")

More Matchers

  • Types
  • Strings
  • Collections
  • Maps
  • Numbers
  • URIs
  • Files
  • Dates
  • Concurrent
  • Futures
  • Threads

Matchers - examples

obj.shouldBeInstanceOf<String>()
service.javaClass.shouldHaveAnnotation(Service::class.java)

collection.shouldContainDuplicates()
list.shouldBeSorted()

uri.shouldHaveHost(host)
uri.shouldHavePath(path)

file.shouldBeLarger(otherFile)
file.shouldBeHidden()

date.shouldHaveSameDayAs(otherDate)

future.shouldBeCancelled()
thread.shouldBeAlive()

Two Styles

  • Extension functions
a.shouldBe(b)
a.shouldStartWith("foo")
a.shouldNotStartWith("foo")
  • Infix functions
a shouldBe b

a shouldStartWith "foo"
a should startWith("foo")

a shouldNotStartWith("foo")
a shouldNot startWith("foo")

Soft Assertions

assertSoftly {
  text shouldHaveMinLength 5
  text shouldStartWith "A"
  text shouldEndWith "Z"
}

Inspectors

class StringSpecExample : StringSpec({
    "Names should be long enough and shouldn't contain space" {
        val names = listOf("John", "Fred", "James")
        names.forAll {
            it.shouldNotContain(" ")
            it.shouldHaveMinLength(4)
        }
    }
})

forNone, forOne, forAny, forAtLeastOne, forAtMost(n), …

Running

Running

  • Gradle
  • Maven
  • IntelliJ IDEA plugin

Listeners

class TestWithListeners : StringSpec() {

    override fun beforeTest(testCase: TestCase) {
        //...
    }

    override fun afterTest(testCase: TestCase, result: TestResult) {
        //...
    }

    override fun beforeSpec(spec: Spec) {
        //...
    }

    override fun afterSpec(spec: Spec) {
        //...
    }
}

Listeners

object MyListener : TestListener {
    override fun beforeTest(testCase: TestCase) {
        //...
    }

    override fun afterTest(testCase: TestCase, result: TestResult) {
        //...
    }

    override fun beforeSpec(spec: Spec) {
        //...
    }

    override fun afterSpec(spec: Spec) {
        //...
    }
}

class TestWithListeners : StringSpec() {
    override fun listeners() = listOf(MyListener)
}

Parallelism

object ProjectConfig : AbstractProjectConfig() {
    override fun parallelism(): Int = 2
}

Test Case Config

class TestCaseConfigTest : FunSpec() {
    init {
        test("Test Case config showcase").config(
                invocations = 6,
                parallelism = 2,
                timeout = 2.seconds,
                enabled = true,
                tags = setOf(Windows, MacOS)
        ) {
            // ...
        }
    }
}

Isolation Modes

SingleInstance (default)
class TestIsolationTest : DescribeSpec() {

    override fun isolationMode(): IsolationMode = IsolationMode.SingleInstance

    init {
        describe("Describe") {
            println("Describe")

            it("It 1") {
                println("It 1")
            }

            it("It 2") {
                println("It 2")
            }
        }
    }
}
// Describe
// It 1
// It 2

Isolation Modes

InstancePerTest
class TestIsolationTest : DescribeSpec() {

    override fun isolationMode(): IsolationMode = IsolationMode.InstancePerTest

    init {
        describe("Describe") {
            println("Describe")

            it("It 1") {
                println("It 1")
            }

            it("It 2") {
                println("It 2")
            }
        }
    }
}
// Describe
// Describe
// It 1
// Describe
// It 2

Isolation Modes

InstancePerLeaf
class TestIsolationTest : DescribeSpec() {

    override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf

    init {
        describe("Describe") {
            println("Describe")

            it("It 1") {
                println("It 1")
            }

            it("It 2") {
                println("It 2")
            }
        }
    }
}
// Describe
// It 1
// Describe
// It 2

Non-deterministic Tests

class EventualTest: StringSpec({
    "should eventually succeed" {
        eventually(5.seconds) {
            // code that might fail at first,
            // but should succeed within the given duration
        }
    }
})

Non-deterministic Tests

class EventualTest: StringSpec({
    "should continually succeed" {
        continually(5.seconds) {
            // code that should succeed
            // and continue to succeed for the given duration
        }
    }
})

Data-driven testing

Table-driven Testing

class TableDrivenTest: StringSpec({
    "square roots" {
      forall(
          row(2, 4),
          row(3, 9),
          row(4, 16),
          row(5, 25)
      ) { root, square ->
        root * root shouldBe square
      }
    }
})

Property-based Testing

  • easy way to write 1000's of tests in no time :-)

Property-based Testing

  1. Describe the input
  2. Describe properties of the output
  3. Try many random inputs, check the properties

Property-based Testing

class StringConcatenationTest: StringSpec({
    "String concatenation" {
            assertAll { a: String, b: String ->
                val concatenated = a + b
                concatenated.length shouldBe a.length + b.length
                concatenated shouldStartWith a
                concatenated shouldEndWith b
            }
        }
})

Generators

  • Provide random inputs
  • Constants for common edge cases (empty string, 0, Int.MAX_VALUE)
  • Standard generators - all common types
  • Custom generators - composition

Shrinkers

  • Try to find minimal input that fails
  • Lowest int, shortest string, shortest list, ...
  • Useful to find the boundaries, where the code breaks
  • KotlinTest only have simple shrinkers
  • Property-base testing - use cases

    • Associativity, commutativity, transitivity, idempotency
    • Fuzzing (monkey testing)
    • Round-trip
    • Alternative implementation
    • Hard to prove, easy to verify

    Extensions

    Spring constructor injection

    object ProjectConfig : AbstractProjectConfig() {
        override fun extensions(): List<ProjectLevelExtension> =
            listOf(SpringAutowireConstructorExtension)
    }
    
    @SpringBootTest
    class SpringInjectionTest(
        val springBean: SpringBean
    ) : StringSpec({
    
        "bean should be injected" {
            springBean shouldNotBe null
        }
    })
    

    Override environment variables

    "Check Environment variables" {
        withEnvironment("env1" to "env value 1") {
            System.getenv("env1") shouldBe "env value 1"
        }
    }

    Test System Exit

    class ExitTest : StringSpec() {
    
        override fun listeners() = listOf(SpecSystemExitListener)
    
        init {
            "Should exit with code 42" {
                val thrown = shouldThrow<SystemExitException> {
                    System.exit(42)
                }
    
                thrown.exitCode shouldBe 42
            }
        }
    }

    Summary

    KotlinTest

    • Test runner & matchers
    • Table-driven testing
    • Property-base testing
    • Idiomatic Kotlin

    Questions?