Kotlin Assertion Libraries – Assertk

— Introduction — AssertJ — Strikt — AssertK — KoTest Assertions — Atrium — Kluent — Conclusions —
Looking at assertk
In this blog post we will be taking a look at version 0.24 of assertk, a library that first appeared back in 2017. It offers, as the name suggests, a largely AssertJ-derived syntax, with several kotlin-specific extensions.
Installation
Installing assertk is a trivial task, all you need is a single dependency that you can find as the first item in assertk’s readme:
1 2 3 4 5 6 7 |
repositories { mavenCentral() } dependencies { testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.24' } |
Syntax
assertk’s syntax will immediately look familiar as its basic components are directly lifted from AssertJ:
1 2 3 4 |
val list: List<Int> = cut.listResult() // [1, 2, 3] assertThat(list).hasSize(3) assertThat(list).containsOnly(1, 2, 3) |
But you don’t need to dig very deep to find some features that take advantage of koltin’s abilities – sometimes for the better, sometimes for the worse. For example the following code will not compile because asserts cannot be made on nullable types straight away:
1 2 3 |
val nullableStr: String? = cut.stringResult() // "The Perfect Flow" assertThat(nullableStr).hasLength(16) |
Nullability issues have to be avoided by explicitly expecting a value to (not) be null:
1 |
assertThat(nullableStr).isNotNull().hasLength(16) |
One might argue that making nullability explicit can make the code – and your intentions behind it – easier to understand. However it is equally true that every other assert you make, like the hasLength(16)
in our example, already contains an implicit – and obvious – non-null requirement. So being forced to add an extra isNotNull()
adds nothing but needless repetition and noise.
On the other hand asserts acting on all elements of a list look better than in AssertJ as you are no longer forced to repeat the assertThat
part:
assertk:
1 2 3 |
assertThat(list).each { num -> num.isPositive() } |
vs
AssertJ:
1 2 3 |
assertThat(list).allSatisfy { num -> assertThat(num).isPositive() } |
Assertions can also be grouped, such that every single assertion will be checked for a full report, regardless of early failures. Grouping works for both a single object
1 2 3 4 5 6 |
val num = cut.bigDecimalResult() // 123.5 assertThat(num).all { isPositive() isBetween(BigDecimal("123.0"), BigDecimal("124.0")) } |
and multiple asserts on different objects
1 2 3 4 |
assertAll { assertThat(cut.stringResult()).contains("Flow") assertThat(cut.listResult()).isNotEmpty() } |
One change that might be noticed negatively, or at least will take some time getting used to, is that unlike AssertJ, assertk’s interface is not fluent (with small exceptions like the isNotNull()
or isInstanceOf()
asserts). For example verifying that an exception has both the correct message and no underlying root cause cannot be done in the style of AssertJ with multiple asserts on the same objects. The code below does not compile because both hasMessage()
and hasNoCause
return Unit
instead of an intermediate Assert
object.
1 2 3 4 5 |
assertThat { cut.failure() } .isFailure() .isInstanceOf(ServiceException::class) .messageContains("XYZ") .hasNoCause() |
To get this test case working again the all
grouping mechanism must be used:
1 2 3 4 5 |
assertThat { cut.failure() }.isFailure().all { isInstanceOf(ServiceException::class) messageContains("Error XYZ") hasNoCause() } |
One curious feature that assertk offers are so called “table asserts” that allow combining multiple sets of disjoint data for your asserts:
1 2 3 4 5 6 |
tableOf("a", "b", "result") .row(0, 0, 1) .row(1, 2, 4) .forAll { a, b, result -> assertThat(a + b).isEqualTo(result) } |
This feature is somewhat reminiscent of JUnit’s parameterised tests, though personally I struggle to come up with a situation where such a feature would not be a code smell. Whatever test ends up working with such a setup is unlikely to be more concise and readable than a few repeated single asserts, or an actual parameterised test.
Error Messages
assertk’s error messages are more detailed than AssertJ’s, as assertk attempts to provide exact information about how the actual result differ from the expectation, instead of just printing both, as AssertJ does. For example in a string comparison that fails because of a typo assertk will point out exactly which letters were different:
1 2 3 |
org.opentest4j.AssertionFailedError: expected [name]:<"T[]e Perfect Flow"> but was:<"T[h]e Perfect Flow"> (Product(name=The Perfect Flow, value=123.5, quantity=5)) Expected :Te Perfect Flow Actual :The Perfect Flow |
Supported Types and Diversity of Assertions
assertk offers a sufficiently versatile set of assertions for the basic String/List/Map/Exception types for you to be able to be productive without many issues. Support for coroutines and and associated types like Flow
is available in a separate library.
However outside of these basics the situation is much less ideal. Common java types like Optional
and Date
have no dedicated support. Same goes for the new java.time
classes like OffsetDateTime
. These are supported only via general Comparable
, so you will need to either write your own asserts or get used to comparing Date
s and DateTime
s with isGreaterThan
and isLessThan instead of isBefore and isAfter
(and if you’re anything like me keep reminding yourself that the later Date
is the one that’s bigger).
Support for numeric types is likewise only available for the general Number
interface. On the one hand that means that there are asserts for all numbers, from Float
s, to BigDecimal
s and AtomicIntegers
. On the other hand it means that some useful special cases, like float comparison with a permissible delta, are missing.
All in all a solid foundation, but lacking on the fringes.
Custom Assertions
Creating your own assertions is quite straight forward, and easy to replicate once you’ve seen how it’s done once or twice. New asserts are written as extension functions, for the Assert<T>
type, using a given
lambda syntax that is reminiscent of BDD descriptions.
There are no special return values to remember, if the assert passes you do nothing, if it doesn’t you just call an expected
helper method with a helpful error message.
For example filling assertk’s Optional
gap would look like this:
(The show
part is a helper method that can be used to wrap or pretty-print values)
1 2 3 4 5 6 7 8 9 10 |
fun <T> Assert<Optional<T>>.contains(value: T) = given { actual -> when { actual.isEmpty -> expected("Optional to contain ${show(value)} but was empty") actual.get() != value -> expected("Optional to contain ${show(value)} but it contained ${show(actual.get())}") else -> return } } |
Resulting in the following error message:
1 |
org.opentest4j.AssertionFailedError: expected Optional to contain <"Vale"> but it contained <"Value"> |
Making a shorter assert that just delegates to checking some other property or callable is also possible. For example making sure an Optional
is empty could be done like this:
1 |
fun <T> Assert<Optional<T>>.isEmpty() = this.prop("Optional Is Empty") { it.isEmpty }.isTrue() |
and would result in an error message that looks like this:
1 |
org.opentest4j.AssertionFailedError: expected [Optional Is Empty] to be true (Optional[Value]) |
Active Development
As of the time of writing this blog assertk has a steady stream of contributions and a small number of open issues and PRs. In addition it is supported by WillowTree and has multiple active contributors. Stagnant development is not an issue.
Quality of Documentation
assertk generally offers short and concise, but easily comprehensible javadoc explanations of its features, often supported by simple examples, such that figuring out what assert you need and what it does is simple enough in the vast majority of cases.
Documentation outside the IDE is more sparse, and only really available in the project’s readme on github. However it does a solid job at filling the gaps on topics like installation instructions and custom assertions. The only thing that is really missing is a proper summary and enumeration of all the supported types and available asserts.
— Introduction — AssertJ — Strikt — AssertK — KoTest Assertions — Atrium — Kluent — Conclusions —
Recent posts






Comment article