Kotlin Assertion Libraries - Atrium

— Introduction — AssertJ — Strike — Assert K — KoTest Assertions — Atrium — Kluent — Conclusions —
Atrium
Disclaimer: This blog post has been updated for atrium 0.18 based on feedback from its author.
Intro
In this blog post we will be looking at version 0.18 of Atrium. It is a relatively young library, having been started in 2017, and promises both a fluent and infix api style, as well as the option to internationalize its error messages.
Installation
Installing atrium is not as straightforward as some of the other libraries presented so far. Already at the installation step you have to make your first choice: do you pick the infix or the fluent syntax style, or maybe both?
Depending on what you pick you need either one or both of these dependencies:
1 2 |
testImplementation("ch.tutteli.atrium:atrium-infix-en_GB:0.18.0") testImplementation("ch.tutteli.atrium:atrium-fluent-en_GB:0.18.0") |
Syntax
You can find a detailed explanation of the differences between the fluent and infix syntax styles in atrium’s official documentation.
The fluent version of the api is not too different from AssertJ, switching out assertThat
for expect
:
1 2 3 4 5 6 7 8 |
val list: List<Int> = cut.listResult() // [1, 2, 3] expect(list) .toHaveSize(3) .toContainElementsOf(listOf(1, 2, 3)) .toHaveElementsAndAll { toBeGreaterThan(0) } .toHaveElementsAndAny { toBeGreaterThan(2) } .toHaveElementsAndNone { toEqual(10) } |
The infix version is almost the same, with the extra weight of expect
making it look like a slightly more verbose version of kotest:
1 2 3 4 5 6 7 |
val list: List<Int?> = cut.listResult() // [1, 2, 3] expect(list) toHaveSize 3 expect(list) toContainElementsOf listOf(1, 2, 3) expect(list) toHaveElementsAndAll { toBeGreaterThan(0) } expect(list) toHaveElementsAndAny { toBeGreaterThan(2) } expect(list) toHaveElementsAndNone { toEqual(10) } |
You can see one of atrium’s idiosyncrasies in the above examples – list assertions that simultaneously verify that a list has elements, and that the elements satisfy a predicate. Another unusual point is that matcher lambda used for the lists is nullable, and passing a null value is a workaround for comparing list elements against null. Simply put: to make sure that no elements of your list are null you don’t do this:
1 |
expect(list) toHaveElementsAndNone { toEqual(null) } |
but this:
1 |
expect(list) toHaveElementsAndNone null |
Another idiosyncrasy is the way atrium handles Exceptions:
1 2 3 4 5 6 7 |
expect { cut.failure() }.toThrow<ServiceException>() .message { toContain("Error") toContain("XYZ") } |
Due to the expect { }
wrapper the actually thrown Exception is not accessible, all you have is an Expect<ServiceException>
intermediary, which is an empty interface without any methods. Using a fluent interface is therefore mandatory, accessing the error like with JUnit’s assertThrows
or Kotest’s shouldThrow
is not possible.
On a distinctly positive note: while nullability requires explicit intervention, it is handled in a sane manner, offering a lambda for further tests after a value has been proven to not be null:
1 2 3 |
expect(nullableStr) notToEqualNull { toContain("Perfect") } |
Assertions on a single object can be grouped to make sure they are all checked, regardless of early failures:
1 2 3 4 5 6 |
val result = cut.bigDecimalResult() // 123.5 expect(result) { toBeLessThan(BigDecimal("1000")) toBeGreaterThan(BigDecimal("55")) } |
And finally you can also build your assertions around arbitrary data extracted from your objects – if you don’t mind the extra syntax that is:
1 2 3 4 5 6 7 8 9 10 11 |
val result = cut.productResult() expect(result) { feature({ f(it::name) }) { toContain("Perfect") toEndWith("Flow") } feature("Quantity", { quantity }) { toBeLessThan(100) } } |
We’ll talk more about the feature
and f
functions when take a look at atrium’s documentation.
Error Messages
atrium’s error messages follow a simple and readable format, listing first the subject (the object being asserted on), followed by a list of failed assertions. That works great for most situations, like simple asserts:
1 2 |
expected that subject: Optional.empty (java.util.Optional <1818339587>) ◆ should contain: "Some Value" <380274260> |
or asserts involving list matching:
1 2 3 4 5 |
expected that subject: [1, 2, 3] (java.util.Arrays.ArrayList <524223214>) ◆ contains, in any order: ⚬ an element which: » is greater than: 5 (kotlin.Int <1511574902>) » but no such element was found |
grouped asserts using features
:
1 2 3 4 5 6 |
expected that subject: Product(name=The Perfect Flow, value=123.5, quantity=5) (asserts.Service.Product <945834881>) ◆ ▶ name: "The Perfect Flow" <225909961> ◾ starts with: "x" <3540494> ◾ ends with: "y" <1209770703> ◆ ▶ Quantity: 5 (kotlin.Int <1186339926>) ◾ is less than: 1 (kotlin.Int <776484396>) |
but can go amiss for objects that do not have a proper toString
implementation like Arrays:
1 2 |
expected that subject: [Ljava.lang.Integer;@4bff7da0 (kotlin.Array <1275035040>) ◆ equals: [Ljava.lang.Integer;@1acaf3d (kotlin.Array <28094269>) |
You may also have noticed that the modules described in the Installation section specify a language – en_GB
– in their name. That is because atrium offers the ability to translate its assertion messages. So far only one other language is available – Swiss German. Just add ch.tutteli.atrium:atrium-translations-de_CH:0.18.0
as another dependency and your error messages turn into proper Denglisch:
1 2 3 4 5 |
expected that subject: [1, 2, 3] (java.util.Arrays.ArrayList <1019484860>) ◆ enthält, in beliebiger Reihenfolge: ⚬ ein Element, welches: » ist grösser als: 5 (kotlin.Int <872522004>) » aber es konnte kein solches Element gefunden werden |
Supported Types and Diversity of Assertions
The basics are covered, but outside of the usual Strings, Lists, Numbers and Maps support gets rather spotty. java.time
classes are covered, but Dates
are not. Optionals
are covered, but outside a dedicated module adding basic support Results
there is nothing more Kotlin-specific. Numeric types are supported only in as much as they are Comparable
, Files
and Classes
have no support.
Such limits are by design, as atrium’s philosophy is to only implement new features based on popular demand, as its readme states:
According to the YAGNI principle this library does not yet offer a lot of out-of-the-box assertion functions. More functions will follow but only if they are used somewhere by someone. So, let us know if you miss something by creating a feature request.
Custom Asserts
Custom assertions in Atrium are written as extension functions for the Expect<T>
type. Your extensions must use something called "_logic"
to createAndAppend
the assertion you are writing:
1 2 3 4 |
infix fun <T> Expect<Optional<T>>.shouldContain(value: T) = _logic.createAndAppend("should contain", value) { it.isPresent && it.get() == value } |
It is also possible to re-use other assertions and group them:
1 2 3 4 5 |
fun Expect<OffsetDateTime>.toBeBetween(lo: OffsetDateTime, hi: OffsetDateTime) = _logic.appendAsGroup { toBeGreaterThanOrEqualTo(lo) toBeLessThanOrEqualTo(hi) } |
A fluent composition is also possible when using the fluent api:
1 2 |
fun Expect<OffsetDateTime>.toBeBetween(lo: OffsetDateTime, hi: OffsetDateTime) = toBeGreaterThanOrEqualTo(lo).and.toBeLessThanOrEqualTo(hi) |
So while the syntax is mostly simple and flexible it also looks like the use of the strange _logic
value is some sort of exposed implementation detail or leaking abstraction. We’ll take a look at, and evaluate, _logic's
official explanation in the next chapter.
Quality of Documentation
Documentation is one of the weaker points of Atrium. Not for lack of trying – every function has a kdoc description and the readme on github is thousands of lines long – but rather because it seems to be written for the wrong target group. Kdoc strings oftentimes read like technical descriptions meant for Atrium’s developers, instead of its users. The github readme (the only source of documentation outside the IDE) likewise often hides useful information in between what feels like paragraphs of digressions.
Let’s look at some concrete examples – the feature
and f
functions that we mentioned when talking about syntax. What is a feature
? Looking at just its signature we can glean that this function operates on a pretty high level of abstraction:
1 2 3 4 |
public fun <T, R> Expect<T>.feature( provider: MetaFeatureOption<T>.(T) → MetaFeature<R>, assertionCreator: Expect<R>.() → Unit ): Expect<T> |
So let’s try the kdoc next:
Extracts a feature out of the current subject of this expectation, based on the given provider, creates a new Expect for it, applies an assertion group based on the given assertionCreator for the feature and returns the initial Expect with the current subject.
That’s a lot of explanations, most of which you will not understand unless you already speak the language of atrium. Let’s try f
next:
Creates a MetaFeature for the given property => use p in case of ambiguity issues. Notice for expectation function writers: you should use feature and pass a class reference instead of using this convenience function (e.g. feature(List::size)). This way we are always able to report the property, even if the subject is not defined which occurs if a previous transformation of the subject could not be carried out.
What about _logic
? It is required to build new assertions, so its documentation should tell us what it is and how it can be used.
Entry point to the logic level of Atrium — which is one level deeper than the API — on which assertion functions do not return Expects but Assertions and the like.
Use _logicAppend in case you want to create and append an Assertion to this Expect.
These explanations sound like they start in medias res, as if the exposition has already been made, and now we need to clear up some technical details. And while these kind of deep details do have their place, there’s precious little in this documentation that would guide us towards being able to write – and understand – the syntax of feature({ f(it::name) })
.
These problems are of course much less severe for the overwhelming majority of assertions you will see in everyday use. These are, much like the other libraries we looked at, largely self-evident and do not really require any special documentation outside a first Hello World example. Unfortunately another flaw is a lack of a list of all possible assertions. The best you can do is a link to auto-generated javadoc, listing every single function and type defined by atrium.
Active Development
Atrium is under active development, commits are made, issues are answered and PRs are merged regularly. However it is maintained largely by a single developer, putting its long-term future into question.
— Introduction — AssertJ — Strikt — AssertK — KoTest Assertions — Atrium — Kluent — Conclusions —
Comment article
Recent posts






Comments
robstoll
Thanks for looking at Atrium, always good to see where people struggle and what they miss or in other words where Atrium should improve. For the readers, since the authors apparently missed a few points:
0. Atrium started in 2017 (first commit 08.01.2017) and not 2018
1. Syntax:
– take a look at https://github.com/robstoll/atrium/blob/main/apis/differences.md if you want to see the real difference between fluent and infix
– Atrium has no extra way to deal with exceptions, every time your subject is a lambda you can use toThrow/notToThrow
– in contrast to other assertion libraries you can fluently assert on the exception and don’t need to start over by extract the result to a variable. An exception is strikt which is not a surprise as strikt was heavily influenced by Atrium (exception syntax, narrowing, feature, grouping) — though they don’t mention it on their page anymore 🙁
2. Supported Types:
– There are out of the box functions for Comparables also in 0.17.0, you even used them in your toBeBetween example 😉
But you are right, we haven’t added support (yet) for types which we never use. But we provide conversion methods such asPath for File and I am going to do the same for Date (was not expecting that anyone is still using java.utilDate as it is pretty much discouraged by anyone). Please state your wishes here: https://github.com/robstoll/atrium/discussions/categories/ideas we happily implement more functions if needed
am
Hi Rob,
thanks for your comment, I’ve made some corrections, hopefully you’ll find the blog more accurate now.
To answer your specific points:
0. That’s fixed. I must’ve miscounted when looking at the numbers in the git log.
1. I’ve added a link to the api documentation. With exceptions I was more missing access to their fields. In hindsight I suppose that this could be arranged using “feature”, which I still think is a pretty heavy syntax if all I want is some custom error code or description from my Exception class.
2. That is an understandable point of view as a maintainer, but as a user I wouldn’t want to go through the process of opening an issue, waiting on an implementation, and then updating my dependencies. At least not for my work where I mostly deal with heavy enterprise projects, and not for something that in the end is as basic a component as an assertions library. Maybe a couple years ago, but I’ve got too many meetings nowadays 😛
I wouldn’t *want* to use Dates either, but sometimes that’s all you get, and in those cases I think it is very convenient to have a few extras beyond just Comparable, like an ‘isBetween(lo, hi)’.
– Alex