Strikt is assertion library built for Kotlin with API that allows building fluent assetions.

To add library to project, add following dependency to your build.gradle

repositories {
  mavenCentral()
}

dependencies {
  testImplementation("io.strikt:strikt-core:0.31.0")
}
To get latest version, visit Github Releases page: https://github.com/robfletcher/strikt/releases

Basic usage

Good assertion library should give us readable assertion errors. Strikt renders error in the following style:

import org.junit.Test
import strikt.api.expectThat
import strikt.assertions.*

class Test {

    @Test
    fun `just checking strikt`() {
    	expectThat("non-empty-string")
            .isNotEmpty()
            .hasLength(12)
    }
}

Running this test prints the following:

▼ Expect that "non-empty-string":
  ✓ is not empty
  ✗ has length 12
         found 16

It is clear which assertion passed and which failed.

More complex assertions

Lets consider more complex example for multi assertion - mapping product entity which may be for some reason discounted:

fun ProductJson.toDisplayable(): ProductDisplayable {
    return ProductDisplayable(
        id = id,
        name = name,
        priceDisplayable = PriceDisplayable(
            fullPrice = price,
            discountPrice = discountPrice,
            currency = currency
        )
    )
}

data class ProductJson(
    val id: String,
    val name: String,
    val price: Double,
    val discountPrice: Double?,
    val currency: String
)

data class ProductDisplayable(
    val id: String,
    val name: String,
    val priceDisplayable: PriceDisplayable
)

data class PriceDisplayable(
    val fullPrice: Double,
    val discountPrice: Double?,
    val currency: String
)
Nothing fancy here, just mapping from one structure to another.

Now let's write assertions on that class fields:

@Test
fun `full product mappings`() {
    val givenProduct =
        ProductJson(
            id = "asd123",
            name = "Product two",
            price = 13.44,
            discountPrice = 9.99,
            currency = "PLN"
        )

    val result = givenProduct.toDisplayable()

    expectThat(result)
        .describedAs("given product with discount")
        .and {
            get { id }.isEqualTo("asd123")
        }
        .and {
            get { name }.isEqualTo("Product two")
        }
        .and {
            get { priceDisplayable.fullPrice }
                .describedAs("Full product price")
                .isEqualTo(13.44)
        }
        .and {
            get { priceDisplayable.discountPrice }
                .describedAs("Discounted product price")
                .isEqualTo(9.99)
        }
}

We can build and chain multiple assertions and still have readable output. Please keep in mind, that with this notation the first assertion fail will terminate the test.  

Handling exceptions

Strikt gives us also API for testing exceptions, consider system under test which will throw UnsupportedOperationException when we try to add items with quantity=0:

class ProductsService {
    fun add(productId: String, count: Int) {
        if (count < 1) {
            throw UnsupportedOperationException("Cannot add 0 items to basket")
        } else {
            //do logic
        }
    }
}

class Test {

    @Test
    fun `it should throw exception`() {
        val productsService = ProductsService()

        expectThrows<UnsupportedOperationException> {
            productsService.add("asd123", 0)
        }.and { get { message }.isEqualTo("Cannot add 0 items to basket") }
    }
}

We can specify type of exception that should be thrown, we put system under test call in function body, and then we have possibility to additionally assert on exception.

See also

Handling exceptions in tests: Junit & Kotest
Exceptions are crucial part of some Java APIs. How to assert that exceptions was thrown? In this note you will examples of various exception assertion techniques.

Soft assertions

Strikt supports also soft assertions - meaning that all assertions in block will be executed and we get the full report. Consider previous example with mapping discounted product:

@Test
fun `full product mappings`() {
    val givenProduct =
        ProductJson(
            id = "asd123",
            name = "Product two",
            price = 13.44,
            discountPrice = 9.99,
            currency = "PLN"
        )

    val result = givenProduct.toDisplayable()

    expect {
        that(result.id).isEqualTo("asd124")
        that(result.name).isEqualTo("Product two")
        that(result.priceDisplayable){
            get { fullPrice }.isEqualTo(13.44)
            get { discountPrice }.isEqualTo(9.99)
        }
    }
}
Wrong values are put in assertion on purpose, to check how Strikt handles fails

Which will result in error:

▼ Expect that "asd123":
  ✗ is equal to "asd124"
          found "asd123"
▼ Expect that "Product two":
  ✓ is equal to "Product two"
▼ Expect that PriceDisplayable(fullPrice=13.44, discountPrice=9.99, currency=PLN):
  ▼ fullPrice:
    ✓ is equal to 13.44
  ▼ discountPrice:
    ✓ is equal to 9.99

See also:

Assert softly - when one assertion is not enough
It’s usually in our best interest to keep one assertion per test method. Yet, situation when more than one check in single test method is present may occur.

Summary

Strikt is not hard to use, gives nice assertion API and gives beautiful assertion error stacktraces. I appreciate  using ✗ and ✓ characters – they increase readability significantly.

There are a lot of assertion frameworks on the market. Using assertion methods built into framework you already use is sufficient almost all the time. Nevertheless it's worth to take a look at Strikt and consider putting it as test dependency in your project.

.