assertk

repository·main·Indexed 21 days ago

https://github.com/assertk-org/assertk

A fluent assertion library for Kotlin, inspired by AssertJ, that provides a type-safe and extensible assertion DSL using extension methods. It supports standard JVM and Kotlin Multiplatform projects, featuring tools for nullability handling, grouped assertions with all() and assertAll(), data extraction via prop(), index(), and key(), and table-based parameterized testing. The assertk-coroutines module adds support for Flow assertions and suspendable functions via suspendCall().

Tokens
4.1K
Snippets
17
Records
17
Agent score
74%

What's inside assertk

  1. Run multiple assertions with all() and assertAll()

    main

    You can group assertions to ensure multiple checks are performed even if one fails.

    1. all { ... }: Use this on a single value to run multiple assertions within a lambda. If any fail, all assertions in the block are executed, and the failure message reports all failures.
    2. assertAll { ... }: Use this to wrap multiple assertThat calls. This ensures that even if one assertThat fails, the subsequent ones are still executed.
    // Multiple assertions on one value
    val string = "Test"
    assertThat(string).all {
        startsWith("L")
        hasLength(3)
    }
    
    // Multiple independent assertions
    assertAll {
        assertThat(false).isTrue()
        assertThat(true).isFalse()
    }
    assertThat(string).all {
        startsWith("L")
        hasLength(3)
    }
    
    assertAll {
        assertThat(false).isTrue()
        assertThat(true).isFalse()
    }
  2. Handle nullability in assertions

    main

    Because nullability is part of Kotlin's type system, assertions on nullable types must be explicit. If you attempt to call a non-null assertion (like hasLength()) on a nullable type, the code will not compile. Use isNotNull() to narrow the type before performing further checks.

    val nullString: String? = null
    
    // This will not compile:
    // assertThat(nullString).hasLength(4)
    
    // Correct way:
    assertThat(nullString).isNotNull().hasLength(4)
    val nullString: String? = null
    assertThat(nullString).isNotNull().hasLength(4)
  3. Basic usage of assertThat()

    main

    Wrap the value or property you want to test in assertThat() and chain assertion methods. You can also provide a custom description as a second argument to assertThat() to improve failure messages.

    import assertk.assertThat
    import assertk.assertions.*
    
    // Asserting on a value
    assertThat(person.name).isEqualTo("Alice")
    
    // Asserting with a custom description
    assertThat(person.age, "age").isGreaterThan(20)
    
    // Asserting on a property reference
    assertThat(person::name).isEqualTo("Alice")
    import assertk.assertThat
    import assertk.assertions.*
    
    assertThat(person.name).isEqualTo("Alice")
  4. Using assertk-coroutines with Turbine

    main

    For complex Flow testing scenarios, it is recommended to use Turbine alongside assertk. Turbine allows you to step through Flow emissions, which you can then validate using assertThat.

    flowOf("one", "two").test {
      assertThat(expectItem()).isEqualTo("one")
      assertThat(expectItem()).isEqualTo("two")
      expectComplete()
    }
  5. Create Custom Assertions

    main

    Since assertk uses Kotlin extension methods, creating custom assertions is straightforward. You can build them in three ways:

    1. Simple Extension Methods

    Wrap existing assertions to create a domain-specific language.

    fun Assert<Person>.hasAge(expected: Int) {
        prop(Person::age).isEqualTo(expected)
    }

    2. Using given for custom failure messages

    Use given to access the actual value and expected()/show() to format custom error messages.

    fun Assert<Person>.hasAge(expected: Int) = given { actual ->
        if (actual.age == expected) return
        expected("age:${show(expected)} but was age:${show(actual.age)}")
    }

    3. Using transform for chaining

    Use transform to assert on a value and then return a new Assert<T> that allows further chaining on a sub-property.

    fun Assert<Person>.hasMiddleName(): Assert<String> = transform(appendName("middleName", separator = ".")) { actual ->
        if (actual.middleName != null) actual.middleName else throw Exception("to not be null")
    }

    Best Practice: Prefer building custom assertions out of existing ones unless you need to provide a significantly more meaningful error message.

    fun Assert<Person>.hasAge(expected: Int) {
        prop(Person::age).isEqualTo(expected)
    }
    
    fun Assert<Person>.hasAge(expected: Int) = given { actual ->
        if (actual.age == expected) return
        expected("age:${show(expected)} but was age:${show(actual.age)}")
    }
    
    fun Assert<Person>.hasMiddleName(): Assert<String> = transform(appendName("middleName", separator = ".")) { actual ->
        if (actual.middleName != null) {
            actual.middleName
        } else {
            expected("to not be null")
        }
    }
  6. Install assertk

    main

    To use assertk in a standard JVM project, add the following to your build.gradle or build.gradle.kts file:

    repositories {
        mavenCentral()
    }
    
    dependencies {
        testImplementation("com.willowtreeapps.assertk:assertk:0.28.1")
    }

    For Kotlin Multiplatform projects, add the dependency to your commonTest source set:

    plugins {
        kotlin("multiplatform")
    }
    
    kotlin {
        sourceSets {
            val commonTest by getting {
                dependencies {
                    implementation("com.willowtreeapps.assertk:assertk:0.28.1")
                }
            }
        }
    }
    dependencies {
        testImplementation("com.willowtreeapps.assertk:assertk:0.28.1")
    }
  7. Use Table-based assertions for structured data

    main

    AssertK provides a Table-based DSL to run the same assertions across multiple rows of data. This is useful for verifying structured inputs and outputs in a single block.

    To use tables:

    1. Create a table using tableOf(...) by specifying column names.
    2. Add rows using the .row(...) method.
    3. Execute assertions for every row using the .forAll { ... } method.

    Tables support up to 4 columns. When an assertion fails, the error message includes the specific row data (e.g., on row:(col1=val1, col2=val2)) to help identify which input caused the failure.

    tableOf("input", "expected")
        .row("a", 1)
        .row("b", 2)
        .forAll { input, expected ->
            // Perform assertions on input and expected
            // e.g., assertThat(input).isEqualTo(expected)
        }
  8. Use suspendCall() to assert on suspendable functions

    main

    The suspendCall() method allows you to perform assertions on the result of a suspendable function. It functions similarly to the standard prop() method but is designed to work within a coroutine scope to handle suspension.

    runBlocking {
        assertThat(person).suspendCall("resume") { it.fetchResume() }.contains("kotlin")
    }
  9. Assert that an exception is thrown

    main

    Use assertFailure with a lambda to verify that a specific block of code throws an exception. You can then chain assertions like hasMessage() to verify the exception details.

    assertFailure {
        throw Exception("error")
    }.hasMessage("wrong")
    assertFailure {
        throw Exception("error")
    }.hasMessage("wrong")
  10. Extract data for assertions

    main

    Use extraction methods to focus assertions on specific parts of an object or collection. These methods improve failure messages by providing context.

    • prop(): Asserts on a property, function, or a named lambda. Works on objects.
    • index(n): Pulls the element at index n from a list.
    • key(k): Pulls the value associated with key k from a map.
    • extracting(selector): Extracts a property from every element in a collection to create a new collection for assertion.
    // Property extraction
    assertThat(person).prop(Person::age).isEqualTo(20)
    
    // Collection index and key extraction
    assertThat(listOf(1, 2, 3)).index(1).isEqualTo(1)
    assertThat(mapOf("two" to 2)).key("two").isEqualTo(2)
    
    // Collection property extraction
    val people = listOf(Person(name = "Sue"), Person(name = "Bob"))
    assertThat(people).extracting(Person::name).containsExactly("Sue", "Bob")
    assertThat(person).prop(Person::age).isEqualTo(20)
    
    assertThat(listOf(1, 2, 3)).index(1).isEqualTo(1)
    
    assertThat(mapOf("two" to 2)).key("two").isEqualTo(2)
    
    assertThat(people).extracting(Person::name).containsExactly("Sue", "Bob")