Kotlin Assertion Libraries - Strikt

— Introduction — AssertJ — Strikt — AssertK — KoTest Assertions — Atrium — Kluent — Conclusions —
Looking at Strikt
Strikt is on the block since May 2018 and is in active development at the time of this post. Strikt itself acknowledges AssertJ as a major influence and it is apparent in its assertion style. Aside from fluent chain assertion, similar to AssertJ, it offers a Kotlin-esque block assertion style.
Installation
Like in many other 3rd party libraries it is sufficent to add the dependency for Strikt to your build tool. The current version and a basic “Getting Started” can be found in the Strikt documentation. For convencience you can find the imports for the version at the time of writing below.
Gradle:
1 2 3 4 5 6 7 |
repositories { mavenCentral() } dependencies { testImplementation("io.strikt:strikt-core:0.31.0") } |
Maven:
1 2 3 4 5 |
<dependency> <groupId>io.strikt</groupId> <artifactId>strikt-jvm</artifactId> <version>0.31.0</version> </dependency> |
Syntax and Error messages
The library offers two entrypoints, which look similar but behave noticably different:
1 2 |
expectThat(subject). //fluent expectThat(subject) {} //block |
Fluent Style
The fluent interface acts comparable to AssertJ’s and allows the chaining of assertions. It will fail the test fast on the first error in the chain.
1 2 3 4 |
val str = "The Perfect Flow" expectThat(str).hasLength(16) .not().containsIgnoringCase("flow") .isNotBlank() |
Corresponding error message
1 2 3 4 |
org.opentest4j.AssertionFailedError: ▼ Expect that "The Perfect Flow": ✓ has length 16 ✗ does not contain "flow" (ignoring case) found "The Perfect Flow" |
Block Style
With block assertions Strikt offers a more Kotlin-style approach. Another thing to note is that the block style behaves like AssertJ’s SoftAssertions and does not fail fast! Additionally it is possible to chain the assertions inside the block. But in this case the assertions will fail fast again for that specific chain.
1 2 3 4 5 6 7 |
val list: List = listOf(1, 2, 3) expectThat(list) { isEmpty().isA<Int>() // Int won't be avaluated due to isEmpty failing. all { isGreaterThan(0) } any { isGreaterThan(2) } none { isEqualTo(10) } } |
Corresponding error message
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
strikt.internal.opentest4j.CompoundAssertionFailure: ▼ Expect that [1, 2, 3]: ✗ is empty ✓ all elements match: ▼ 1: ✓ is greater than 0 ▼ 2: ✓ is greater than 0 ▼ 3: ✓ is greater than 0 ✓ at least one element matches: ▼ 1: ✗ is greater than 2 ▼ 2: ✗ is greater than 2 ▼ 3: ✓ is greater than 2 ✓ no elements match: ▼ 1: ✗ is equal to 10 found 1 ▼ 2: ✗ is equal to 10 found 2 ▼ 3: ✗ is equal to 10 found 3 |
This example also showcases two improvements over AssertJ. The assertion of collections is more concise with all/any/none and the error messages are by leaps and bounds more detailed.
Finally there is an expect block which is used to encompass multiple subjects. It behaves the same as the single subject expect block.
1 2 3 4 5 6 7 8 |
expect { that(str).hasLength(16) .not().containsIgnoringCase("flow") .isNotBlank() that(123) { isA<Int>() } } |
Another prominent difference to AssertJ is the way to assert exceptions.
1 2 3 4 5 6 |
expectCatching { error("Error XYZ") } .isFailure() .isA<IllegalStateException> .message.isEqualTo("Error XYZ") expectThrows<IllegalStateException> { error("Error XYZ") } //Shorthand .and { message.isEqualTo("Error XYZ") } |
Not really intuitive is the way to assert that no exception was thrown.
1 2 |
expectCatching { print("Success!") } .isSuccess() |
Nullability and Narrowing
Strikt’s fluidity is built upon a builder pattern which determines the type of the subject assertions are made against. This leads to the effect that we can only access type specific assertions after determining the subject’s type. A familiar behavior looking back to AssertJ.
That means for a nullable type we have to narrow it down to the actual type before we can verify the underlying data.
1 |
expectThat(nullableStr).isNotNull().contains("Perfect") |
This holds true for narrowing down types which are not known until runtime
1 2 |
val list: List<Any> = listOf(1, 2, 3) expectThat(list[0]).isA<Int>().isContained(1..3) |
Whether narrowing down nullable types adds understanding or is just fluff remains debatable, but it aligns with the overall architectural pattern of Strikt.
Notable functionality: Extracting
Strikt provides a very convenient functionality akin to AssertJ’s extracting with its subject traversing get (or map for collections). Additionally you can build convenience extension functions to reuse traversals.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
expectThat(product) { get(Product::name).isEqualTo("The Perfect Flow") get(Product::value).isEqualTo(BigDecimal(123.5)) get(Product::quantity).isEqualTo(5) } expectThat(product) { name.isEqualTo("The Perfect Flow") value.isEqualTo(BigDecimal(123.5)) quantity.isEqualTo(5) } val Assertion.Builder<Product>.name: Assertion.Builder<String> get() = get(Product::name) val Assertion.Builder<Product>.value: Assertion.Builder<BigDecimal> get() = get(Product::value) val Assertion.Builder<Product>.quantity: Assertion.Builder<Int> get() = get(Product::quantity) |
Supported Types and Diversity of Assertions
Strikt uses a modularized architecture to support different types of assertions. The main dependency, the strikt-core, covers most of Kotlin’s basic types as well as Kotlin types like Collections, Enum, Result, tuples like Pair and ClosedRanges.
The JVM module adds support for java.io.File and java.nio.Path as well as Optional and a selection of TemporalAccessors.
That being said, the selection of available assertions for the supported types has more support for edge cases in AssertJ but the bases are definitely covered. Furthermore, the constraints on the TemporalAccessors can make it necessary to write dedicated custom assertions for types like Date.
There are several other modules available, which bring support for e.g. Spring and Arrow.
Custom Assertions
Creating your own custom assertions is as simple as it can be. New assertions are built as extension functions upon the Assertion.Builder<T> class, with T being the type to be asserted. For simple assertions we can use a very concise form.
1 |
fun Assertion.Builder<Product>.hasSameNameAs(other: Product) = assertThat("Same Productname") { it.name == other.name } |
A little more sophisticated it can look like this:
1 2 3 4 5 6 7 8 |
fun <T> Assertion.Builder<Optional<T>>.contains(value: T) = assert("has value %s", value) { when { it.isEmpty -> fail("Optional to contain $value but was empty") it.get() != value -> fail("Optional to contain $value but it contained ${it.get()}") else -> pass(actual = it.get()) } } |
Active Development
At the time of writing, the library is under active development, albeit with bigger gaps in activity. For now the API is also subject to change until 1.0 is released.
Aside from robfletcher there is a handful of other contributors. If the library can make further progress remains to be seen.
Quality of Documentation
Strikt generally offers short and concise, but easily comprehensible javadoc explanations of its features as well as mostly self-explanatory method names, so that figuring out what assertion you need and what it does is simple enough in the vast majority of cases.
Documentation outside of the IDE encompasses everything you need on its own website. The only thing that is really missing is a proper overview over all the supported types and available assertions.
— Introduction — AssertJ — Strikt — AssertK — KoTest Assertions — Atrium — Kluent — Conclusions —
Recent posts






Comment article