

Atrium is an open-source multiplatform expectation/assertion library for Kotlin with support for JVM, JS and Android.
It is designed to support multiple APIs, focuses on helping developers to understand what went wrong and prevents common pitfalls.
The project was inspired by AssertJ at first but moved on and provides now more
flexibility, features and hints to its users (so to you 😉).
Atrium is designed to be extensible as well as configurable
and allows you to extend it with your own expectation functions, customise reporting
or even replace core components with your own implementation easily.
See Examples below to get a feel for how you could benefit from Atrium.
❗ You are taking a sneak peek at the next version. It could be that some features you find on this page are not released yet.
Please have a look at the README of the corresponding release/git tag. Latest version: README of v1.2.0.
Table of Contents
Installation
Atrium is published to mavenCentral.
It has the following minimum requirement:
- Kotlin: 1.4
- JVM: 11
- JS: IR-Backend (LEGACY support was dropped with Atrium 1.2.0)
In case you use Kotlin 1.5 or newer, then regardless of the target platform, you can use the following group and artifactId
build.gradle.kts:
repositories {
mavenCentral()
}
dependencies {
testImplementation("ch.tutteli.atrium:atrium-fluent:1.2.0")
}
And in case of an MPP project accordingly:
repositories {
mavenCentral()
}
kotlin {
sourceSets {
val commonTest by getting {
implementation("ch.tutteli.atrium:atrium-fluent:1.2.0")
}
}
}
Exchange fluent with infix depending on your taste (see API styles for more information).
That is all, you are all set. Jump to Examples which shows how to use Atrium.
I use a version prior to Kotlin 1.5
In case you use a version prior to Kotlin 1.5, then use the following depending on the target platform:
- common: atrium-fluent
- jvm: atrium-fluent-jvm
- android: atrium-fluent-jvm
- js: atrium-fluent-js
I have other problems: please take a look at the Sample Projects for further guidance.
Sample Projects
Have a look into the samples
folder, it currently contains sample projects for
Are you using a different runner? A PR would be appreciated 😊.
Examples
We are using the API provided by the bundle module
atrium-fluent
in the following examples.
It provides a pure fluent API for the JVM platform.
Have a look at
apis/differences.md
to see how the infix API looks like, how they differ respectively.
Your First Expectation
See also AnyExpectationSamples
for further examples.
We start off with a simple example:
import ch.tutteli.atrium.api.fluent.en_GB.*
import ch.tutteli.atrium.api.verbs.expect
val x = 10
expect(x).toEqual(9)
↑ Example ↓ Output
I expected subject: 10 (kotlin.Int <1234789>)
◆ to equal: 9 (kotlin.Int <1234789>)
The statement can be read as "I expect x to equal nine" where an equality check is used (for an identity check, you would have to use toBeTheSameInstace).
Since this is false, an AssertionError is thrown with a corresponding message as shown in the Output
where on the first line the actual subject (10 in the above example) is shown and on following lines which start with,
◆ ... (here only one) we see the expectations we had about the subject
In this sense the report can be read as
-- and needlessly to say, this expectation was not met and thus the thrown error.
We are using the bundle atrium-fluent
and the predefined expectation verb expect in the examples.
Thus, the corresponding imports at the beginning of the file in the above example.
We will omit the import statements in the remaining examples for brevity.
You want to run the examples yourself?
Have a look at the Installation section which explains how to set up a dependency to Atrium.
The next section shows how you can define multiple expectations for the same subject.
Define Single Expectations or an Expectation-Group
expect(4 + 6).toBeLessThan(5).toBeGreaterThan(10)
↑ Example ↓ Output
I expected subject: 10 (kotlin.Int <1234789>)
◆ to be less than: 5 (kotlin.Int <1234789>)
Atrium allows you to chain expectations or in other words
you only need to write the expect(...) part once and can state several single expectations for the same subject.
The expression which determines the subject of the expectations (4 + 6 in the above example) is evaluated only once.
In this sense we could have written it also as follows (which is only the same because 4 + 6 does not have side effects).
expect(4 + 6).toBeLessThan(5)
expect(4 + 6).toBeGreaterThan(10)
The first expect statement throws an AssertionError as it does not hold.
In the above example, toBeLessThan(5) is already wrong and thus toBeGreaterThan(10) was not evaluated at all
and correspondingly not reported.
If you want that both expectations are evaluated together, then use the expectation-group syntax as follows:
expect(4 + 6) {
toBeLessThan(5)
toBeGreaterThan(10)
}
↑ Example ↓ Output
I expected subject: 10 (kotlin.Int <1234789>)
◆ to be less than: 5 (kotlin.Int <1234789>)
◆ to be greater than: 10 (kotlin.Int <1234789>)
An expectation-group throws an AssertionError at the end of its block (i.e. at the closing });
hence reports that both expectations do not hold.
The reporting can be read as I expected the subject of the expectation, which was 10, to be less than 5 and to be greater than 10
You can use and as filling element between single expectations and expectation-groups:
expect(5).toBeGreaterThan(2).and.toBeLessThan(10)
expect(5) {
} and {
}
Soft-Expectations
An expectation-group is similar to the concept of
soft assertions in AssertJ although with a few differences:
- you do not need an extra utility such as
assertSoftly if you define expectations about the same subject,
you can just use expect as always.
- you do not have to repeat the subject
The above example is the equivalent of the following AssertJ example:
assertSoftly {
assertThat(4 + 6).isLessThan(5)
assertThat(4 + 6).isGreatThan(10)
}
fun assertSoftly(body: SoftAssertions.() -> Unit) =
SoftAssertions.assertSoftly(body)
Moreover, in contrast to AssertJ, the block syntax is provided at many places and not only on the top-level.
As an example, the following AssertJ example:
assertSoftly {
assertThat(mansion.numOfGuests).isEqualTo(7)
assertThat(mansion.kitchen.status).isEqualTo("clean")
assertThat(mansion.kitchen.numOfTables).isGreaterThan(5).isLessThan(10)
}
fun assertSoftly(body: SoftAssertions.() -> Unit) =
SoftAssertions.assertSoftly(body)
could be written as follows in Atrium (see also Feature Extractors).
expect(mansion) {
its { numOfGuests }.toEqual(7)
its({ kitchen }) {
its { status }.toEqual("clean")
its { numOfTables }.toBeGreaterThan(5).toBeLessThan(10)
}
}
💬 fail-fast in expectation-groups
Note that you are free to choose the dot-notation (e.g. toBeGreaterThan(5).toBeLessThan(10)) at any level, however
once you are within an expectation-group block, all of them are evaluated (no more fail-fast behaviour applies).
In other words, toBeLessThan(10) is still reported, even though toBeGreaterThan(5) already fails
in the above example.
If you want to state expectations about multiple unrelated subjects and want to report them together (or introduce groups),
then you might be interested in using expectGrouped instead of expect -> take a look at the data driven testing section.
Expect an Exception
See also Fun0ExpectationSamples
for further examples.
expect {
throw IllegalArgumentException("name is empty")
}.toThrow<IllegalStateException>()
↑ Example ↓ Output
I expected subject: () -> kotlin.Nothing (readme.examples.ToThrowExamples$ex-toThrow1$1 <1234789>)
◆ ▶ thrown exception when called: java.lang.IllegalArgumentException
◾ to be an instance of type: IllegalStateException (java.lang.IllegalStateException)
ℹ Properties of the unexpected IllegalArgumentException
» message: "name is empty" <1234789>
» stacktrace:
⚬ readme.examples.ToThrowExamples$ex-toThrow1$1.invoke(ToThrowExamples.kt:18)
⚬ readme.examples.ToThrowExamples$ex-toThrow1$1.invoke(ToThrowExamples.kt:16)
⚬ readme.examples.ToThrowExamples.ex-toThrow1(ToThrowExamples.kt:47)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
⚬ java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
⚬ java.base/java.lang.reflect.Method.invoke(Method.java:566)
You can also pass a lambda to expect and then use toThrow to state the expectation that
invoking the lambda throws a certain exception (IllegalStateException in the example above).
As with all narrowing functions, there are two overloads:
- the first expects an
assertionCreator-lambda in which you can define sub-expectations.
An assertionCreator-lambda has always the semantic of an expectation-group.
It has also the benefit, that Atrium can show those sub-expectations in error reporting,
even if a failure happens before, giving some additional context to a failure.
- the second overload expects all the parameters except the
assertionCreator-lambda and turns the subject into the expected type;
failing to do so cannot include additional information in error reporting though.
The following example uses the first overload
expect {
throw IllegalArgumentException()
}.toThrow<IllegalArgumentException> {
message { toStartWith("firstName") }
}
↑ Example ↓ Output
I expected subject: () -> kotlin.Nothing (readme.examples.ToThrowExamples$ex-toThrow2$1 <1234789>)
◆ ▶ thrown exception when called: java.lang.IllegalArgumentException
◾ ▶ message: null
◾ not to equal: null but to be an instance of: String (kotlin.String) -- Class: java.lang.String
» to start with: "firstName" <1234789>
And this one uses the second overload; notice the difference in reporting,
this one does not include what sub-expectations would have been made if the narrowing succeeded
expect {
throw IllegalArgumentException()
}.toThrow<IllegalArgumentException>().message.toStartWith("firstName")
↑ Example ↓ Output
I expected subject: () -> kotlin.Nothing (readme.examples.ToThrowExamples$ex-toThrow3$1 <1234789>)
◆ ▶ thrown exception when called: java.lang.IllegalArgumentException
◾ ▶ message: null
◾ not to equal: null but to be an instance of: String (kotlin.String) -- Class: java.lang.String
As side notice, message is a shortcut for feature(Throwable::message).notToEqualNull,
which creates a feature extractor (see next section) about Throwable::message.
There is also the counterpart of toThrow named notToThrow:
expect {
throw IllegalArgumentException("name is empty", RuntimeException("a cause"))
}.notToThrow()
↑ Example ↓ Output
I expected subject: () -> kotlin.Nothing (readme.examples.ToThrowExamples$ex-notToThrow$1 <1234789>)
◆ ▶ invoke(): ❗❗ threw java.lang.IllegalArgumentException
ℹ Properties of the unexpected IllegalArgumentException
» message: "name is empty" <1234789>
» stacktrace:
⚬ readme.examples.ToThrowExamples$ex-notToThrow$1.invoke(ToThrowExamples.kt:42)
⚬ readme.examples.ToThrowExamples$ex-notToThrow$1.invoke(ToThrowExamples.kt:40)
⚬ readme.examples.ToThrowExamples.ex-notToThrow(ToThrowExamples.kt:43)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
⚬ java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
⚬ java.base/java.lang.reflect.Method.invoke(Method.java:566)
» cause: java.lang.RuntimeException
» message: "a cause" <1234789>
» stacktrace:
⚬ readme.examples.ToThrowExamples$ex-notToThrow$1.invoke(ToThrowExamples.kt:42)
Notice that stacks are filtered so that you only see what is of interest.
You can use your own
AtriumErrorAdjuster
to adjust the filtering.
Stack frames of Atrium and of test runners (JUnit, Kotest, TestNG and Spek for JVM, mocha and jasmine for JS) are excluded per default.
Create a Feature Request
in case you use a different runner, we can add yours to the list as well.
Feature Extractors
See also FeatureExtractorSamples
for further examples.
Many times you are only interested in certain features of the subject and want to state expectations about them.
There are different use cases for feature extractors.
We will start of with properties and method calls and go on with more complicated scenarios.
Property and Methods
We are using the data class Person in the following examples:
The simplest way of defining expectations for a property of an instance or for the return value of a method call is by
using the extension method its.
expect(myPerson)
.its({ isStudent }) { toEqual(true) }
.its { fullName() }
.toStartWith("rob")
↑ Example ↓ Output
I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false) (readme.examples.FeatureExtractorExamples.Person <1234789>)
◆ ▶ its.definedIn(FeatureExtractorExamples.kt:35): false
◾ to equal: true
In the above example we created two expectations, one for the property isStudent of myPerson
and a second one for the return value of calling fullName() on myPerson.
A feature extractor is indicated as follows in reporting:
It starts with a ▶ followed by the feature's description and its actual value.
So the above output can be read as
I expected the subject of the expectation, which was actually Person(...),
respectively its property which was defined in FeatureExtractorSpec.kt on line 43,
which was actually false, to equal true.
The second feature is not shown in reporting as the first expectation about the property isStudent already failed,
and we have chosen to use single expectations
which have fail-fast semantic.
Feature extractors follow the common pattern of having two overloads:
One drawback of its (which we plan to improve but most likely not before we drop support for Kotlin < 1.5) is that reading the resulting
feature description does not immediately tell us what feature we extracted.
That is where the feature function comes into play. It is based on reflection and uses the name of the feature
as description. Following the first example rewritten to feature.
expect(myPerson)
.feature({ f(it::isStudent) }) { toEqual(true) }
.feature { f(it::fullName) }
.toStartWith("rob")
↑ Example ↓ Output
I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false) (readme.examples.FeatureExtractorExamples.Person <1234789>)
◆ ▶ isStudent: false
◾ to equal: true
The report reads much nicer now:
I expected the subject of the expectation,
which was actually Person(...), respectively its property isStudent,
which was actually false, to equal true
The drawback of feature compared to its is its syntax. Certainly, one has to get used to it first. Another is that
you might run into Ambiguity Problems due to Kotlin bugs.
feature has several overloads, we are looking at the one expecting a lambda in which you have to provide a MetaFeature.
Creating a MetaFeature is done via the function f by passing in a
bounded reference
of the corresponding property or method (including arguments if required).
within the -provider-lambda refers to the subject of the expectation ( in the above example).
Also feature follows the common pattern of having two overloads where the first expects an assertionCreator-lambda and
the second has the same parameters except the assertionCreator-lambda and changes the subject to the feature,
meaning a subsequent call in the fluent chain is about the feature and not the previous subject.
Following the second example rewritten from its to feature:
expect(myPerson) {
feature({ f(it::firstName) }) {
toStartWith("Pe")
toEndWith("er")
}
feature { f(it::lastName) }.toEqual("Dummy")
}
↑ Example ↓ Output
I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false) (readme.examples.FeatureExtractorExamples.Person <1234789>)
◆ ▶ firstName: "Robert" <1234789>
◾ to start with: "Pe" <1234789>
◾ to end with: "er" <1234789>
◆ ▶ lastName: "Stoll" <1234789>
◾ to equal: "Dummy" <1234789>
Atrium provides several shortcuts for commonly used properties so that you can use them instead of writing its { ... } / feature(...) all the time.
For instance, message for Throwable (see Expect an Exception), first and for and others.
Please in case you miss a shortcut.
💬 <- this icon signifies answers/input for advanced users, you might want to skip them if you are new to Atrium.
Last but not least, let us have a look at an example where a method with arguments is used as feature:
expect(myPerson)
.feature { f(it::nickname, false) }
.toEqual("Robert aka. Stoll")
.toStartWith("llotS")
↑ Example ↓ Output
I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false) (readme.examples.FeatureExtractorExamples.Person <1234789>)
◆ ▶ nickname(false): "Mr. Robert" <1234789>
◾ to equal: "Robert aka. Stoll" <1234789>
f supports methods with up to 5 arguments.
Atrium provides shortcuts for commonly used methods, e.g. List.get, Map.getExisting, Optional.toBePresent or Result.toBeSuccess
where all of them include some additional checking (index bound, existence of the key within the map etc.)
Please open a feature request
in case you miss a shortcut.
💬 Write own feature extractors with additional checks.
Atrium provides a feature extractor which allows to extract in a safe way in case the extraction is only valid for certain subjects.
It is inter alia used for List.get
Arbitrary Features
A feature does not necessarily have to be directly related to the subject as properties or method calls do.
Either use its or the overload of feature which expects a feature description in form of a String as first argument.
Following an example using feature.
data class FamilyMember(val name: String)
data class Family(val members: List<FamilyMember>)
val myFamily = Family(listOf(FamilyMember("Robert")))
expect(myFamily)
.feature("the number of members", { members.size }) { toEqual(1) }
.feature() { members.first().name }
.toEqual()
↑ Example ↓ Output
I expected subject: Family(members=[FamilyMember(name=Robert)]) (readme.examples.FeatureExtractorExamples.Family <1234789>)
◆ ▶ the first member's name: "Robert" <1234789>
◾ to equal: "Peter" <1234789>
Also, this version of feature provides two different kind of overloads:
- the first expects a feature description, a feature-provider-lambda and an
assertionCreator-lambda, in which you can define sub-expectations for the feature.
An assertionCreator-lambda has always the semantic of an expectation-group or in other words, not-fail fast.
It has also the benefit, that Atrium can provide those sub-expectations in error reporting,
Moreover, the subject stays the same so that subsequent calls are still about the same subject.
- the second overload expects all the parameters except the
assertionCreator-lambda and changes the subject to the feature,
meaning a subsequent call in the fluent chain is about the feature and not the previous subject.
As you can see, Atrium provides a generic way to postulate expectations about features.
Yet, if you extract the same feature over and over again or it gets more complicated,
then it might be worth to write an own expectation function where we recommend to
use feature over its.
Within Expectation Functions / Feature Extractors
In case you write an own expectation function, then we discourage two things:
- using
its because the reporting reads less nice and it is also less efficient than feature
- using
feature with a MetaFeature-provider-lambda (as shown in Property and Methods)
Instead, we encourage you to pass a class references
to feature.
This has the benefit, that we can always show the feature name, also in case a previous feature extraction or subject
transformation failed.
Following an example:
fun <F : Any, T : Pair<F, *>> Expect<T>.firstToBeDoneWrong(expected: F) =
feature({ f(it::first) }) { toEqual(expected) }
> Expect. =
feature(Pair<F, *>::first) { toEqual(expected) }
(listOf( to , to )).() {
firstToBeDoneWrong()
firstToBe()
}
↑ Example ↓ Output
I expected subject: [(1, a), (2, b)] (java.util.Arrays.ArrayList <1234789>)
◆ ▶ get(10): ❗❗ Index out of bounds
» ▶ CANNOT show description as it is based on subject which is not defined:
◾ to equal: 1 (kotlin.Int <1234789>)
» ▶ first:
◾ to equal: 1 (kotlin.Int <1234789>)
Also, this version of feature provides two kind of overloads, one without and one with assertionCreator-lambda.
(see for instance Arbitrary Features for more information).
Ambiguity Problems
Unfortunately there are several Kotlin bugs when it comes to overloading, especially in conjunction with KFunction
(see and upvote in case you run into one).
It might happen that you run into such issues using in conjunction with a -provider-lambda (as shown in ).
However, Atrium provides alternative functions next to within the -provider-lambda to disambiguate the situation.
Use for properties and to for methods.
Likely you need to specify the type parameters manually as Kotlin is not able to infer them correctly.
Notice, that you might run into the situation that Intellij is happy but the compiler is not.
For instance, Intellij will suggest that you can remove the type parameters in the above example.
Yet, if you do so, then the compiler will fail, mentioning ambiguous overloads.
Most of the time this problem stems from the reason that Intellij is using a newer Kotlin version to analyse
than the one you compile your project with.
Next to using the alternative functions, you could also use its or the overload of feauture which expects
a String as description (as shown in arbitrary features.
Property does not exist
In case you deal with Java code and are using feature, then you might run into the problem that a property does not exist.
This is due to the fact that Kotlin only provides syntactic sugar to access a getter via property syntax.
In such a case, use the get... method instead. For instance:
class A {
public String getFoo() { return "bar"; }
}
val a = A()
a.foo
expect(a)
.feature { f(it::getFoo) }
.startsWith(...)
Subject Extraction
extractSubject allows to get hold on the subject of the current Expect in case it is defined and reports an error
otherwise. There is rarely a good reason to use it as there are better options:
- feature extractor (
feature, its or _logic.extractFeature...)
- toHoldThirdPartyExpectation (see Integrate other Assertion/Expectation Libraries).
- subject changer (_logic.changeSubject...)
The only case where it makes sense (which we are aware of so far) is, if your method under test generates random results
(e.g. a data generator) and you want to state expectations which depend on the random generated data.
For instance:
val persons = dataGenerator.getRandomPersonsWithChildren()
expect(persons).toHaveElementsAndAll {
extractSubject { person ->
feature { f(it::children) }.notToHaveElementsOrAll {
because("person should at least be 16 years older than its children") {
feature { f(it::age) }.toBeLessThan(person.age - 16)
}
}
}
}
Type Expectations
See also AnyExpectationSamples -> toBeAnInstanceOf and co.
for further examples.
interface SuperType
data class SubType1(val number: Int) : SuperType
data class SubType2(val word: String, val flag: ) : SuperType
x: SuperType = SubType2(, flag = )
(x).toBeAnInstanceOf<SubType2> {
feature { f(it::word) }.toEqual()
feature { f(it::flag) }.toEqual()
}
↑ Example ↓ Output
I expected subject: SubType2(word=hello, flag=true) (readme.examples.SubType2 <1234789>)
◆ ▶ word: "hello" <1234789>
◾ to equal: "goodbye" <1234789>
◆ ▶ flag: true
◾ to equal: false
You can narrow the type of the subject with the toBeAnInstanceOf function.
On one hand it checks that the subject of the current expectation (x in the above example) is actually the expected type
and on the other hand it turns the subject into this type.
This way you can make specific expectations which are only possible for the corresponding type
-- for instance, considering the above example, number is not available on SuperType but only on SubType1.
expect(x).toBeAnInstanceOf<SubType1>()
.feature { f(it::number) }
.toEqual(2)
↑ Example ↓ Output
I expected subject: SubType2(word=hello, flag=true) (readme.examples.SubType2 <1234789>)
◆ to be an instance of type: SubType1 (readme.examples.SubType1)
There are two toBeAnInstanceOf overloads:
- the first (shown in the first example) expects an
assertionCreator-lambda in which you can define sub-expectations.
An assertionCreator-lambda has always the semantic of an expectation-group
-- as a recapitulation, expectations in an expectation-group are all evaluated and failures are reported at the end of the block.
It has also the benefit, that Atrium can provide those sub-expectations in error reporting,
showing some additional context in case of a failure.
- the second overload (shown in the second example) is parameterless and turns only the subject into the expected type;
failing to do so cannot include additional information in error reporting though.
Nullable Types
See also AnyExpectationSamples -> notToEqualNullFeature and co.
for further examples.
Let us look at the case where the subject of the expectation has a nullable type.
val slogan1: String? = "postulating expectations made easy"
expect(slogan1).toEqual(null)
↑ Example ↓ Output
I expected subject: "postulating expectations made easy" <1234789>
◆ to equal: null
val slogan2: String? = null
expect(slogan2).toEqual("postulating expectations made easy")
↑ Example ↓ Output
I expected subject: null
◆ to equal: "postulating expectations made easy" <1234789>
On one hand, you can use toEqual and pass the same type --
String? in the above example, so in other words either null as in the first example or a String as in the second example.
On the other hand, you can use notToEqualNull to turn the subject into its non-null version.
Following an example:
expect(slogan2)
.notToEqualNull()
.toStartWith("atrium")
↑ Example ↓ Output
I expected subject: null
◆ not to equal: null but to be an instance of: String (kotlin.String) -- Class: java.lang.String
notToEqualNull provides two overloads:
one without (example above) and one with assertionCreator-lambda (example below); see
Type Expectations for more information on the difference of the overloads.
expect(slogan2).notToEqualNull { toStartWith("atrium") }
↑ Example ↓ Output
I expected subject: null
◆ not to equal: null but to be an instance of: String (kotlin.String) -- Class: java.lang.String
» to start with: "atrium" <1234789>
Atrium provides one additional function which is intended for data driven testing
involving nullable types and is explained in the corresponding section.
👓 <- this icon signifies additional information, worth reading in our opinion but if you are only after code examples,
then you can skip now to the next section (otherwise click on the arrow to expand the section).
👓 dealing a lot with nullable types from Java...
... in this case we recommend having a look at the Java Interoperability section.
Collection Expectations
Atrium provides expectation builders which allow to state sophisticated toContain expectations about Iterable<T>.
Such a building process allows you to define very specific expectations, where the process is guided by a fluent builder pattern.
You can either use such an
Expectation Builder
to create a specific expectation or use one of the
Shortcut Functions in case you have kind of a common case.
The following sub sections show both use cases by examples.
Shortcut Functions
See also
IterableExpectationSamples
for further examples.
expect(listOf(1, 2, 2, 4)).toContain(2, 3)
↑ Example ↓ Output
I expected subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>)
◆ to contain, in any order:
⚬ an element which equals: 3 (kotlin.Int <1234789>)
» but no such element was found
The expectation function toContain(2, 3) is a shortcut for using a
Sophisticated Expectation Builder -- it actually calls toContain.inAnyOrder.atLeast(1).values(2, 3).
This is reflected in the output.
👓 and what about expected value 2?
Exactly, what about the expected value 2, why do we not see anything about it in the output?
The output does not show anything about the expected value 2 because the default reporter reports only failing expectations.
Back to the shortcut functions.
Next to expecting that certain values are contained in or rather returned by an Iterable,
Atrium allows us to use an assertionCreator-lambda to identify an element
(an assertionCreator-lambda can also be thought of as a matcher / predicate in this context).
An element is considered as identified, if it holds all specified expectations the assertionCreator creates.
Following an example:
expect(listOf(1, 2, 2, 4)).toContain(
{ toBeLessThan(0) },
{ toBeGreaterThan(2).toBeLessThan(4) }
)
↑ Example ↓ Output
I expected subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>)
◆ to contain, in any order:
⚬ an element which needs:
» to be less than: 0 (kotlin.Int <1234789>)
» but no such element was found
⚬ an element which needs:
» to be greater than: 2 (kotlin.Int <1234789>)
» to be less than: 4 (kotlin.Int <1234789>)
» but no such element was found
In the above example, neither of the two lambdas matched any elements and thus both are reported as failing (sub) expectations.
Another shortcut function which Atrium provides for is kind of
the opposite of and is named .
Again, Atrium provides two overloads for it, one for values,
e.g. which calls
and a second one which expects one or more -lambda,
e.g.
which calls .
We will spare the examples here and show them in the following sections.
Notice that you can pass to instead of an -lambda to match .
This makes of course only sense if your contains nullable elements.
Atrium provides also a notToContain shortcut function.
Furthermore, it provides aliases for toContain and notToContain named toHaveElementsAndAny and
toHaveElementsAndNone, which might be a better choice if you think in terms of: I expect a predicate holds.
These two are completed with an toHaveElementsAndAll expectation function.
Following each in action:
expect(listOf(1, 2, 3, 4)).toHaveElementsAndAny {
toBeLessThan(0)
}
↑ Example ↓ Output
I expected subject: [1, 2, 3, 4] (java.util.Arrays.ArrayList <1234789>)
◆ to contain, in any order:
⚬ an element which needs:
» to be less than: 0 (kotlin.Int <1234789>)
» but no such element was found
expect(listOf(1, 2, 3, 4)).toHaveElementsAndNone {
toBeGreaterThan(2)
}
↑ Example ↓ Output
I expected subject: [1, 2, 3, 4] (java.util.Arrays.ArrayList <1234789>)
◆ not to contain:
⚬ an element which needs:
» to be greater than: 2 (kotlin.Int <1234789>)
❗❗ following elements were mismatched:
⚬ index 2: 3 (kotlin.Int <1234789>)
⚬ index 3: 4 (kotlin.Int <1234789>)
expect(listOf(1, 2, 3, 4)).toHaveElementsAndAll {
toBeGreaterThan(2)
}
↑ Example ↓ Output
I expected subject: [1, 2, 3, 4] (java.util.Arrays.ArrayList <1234789>)
◆ elements need all:
» to be greater than: 2 (kotlin.Int <1234789>)
❗❗ following elements were mismatched:
⚬ index 0: 1 (kotlin.Int <1234789>)
⚬ index 1: 2 (kotlin.Int <1234789>)
Sophisticated Expectation Builders
Sophisticated expectation builders implement a fluent builder pattern.
To use the expectation builder for sophisticated Iterable<T>-toContain-expectations, you can type toContain
-- as you would when using the Shortcut Functions toContain --
but type as next step (so that you are using the property instead of one of the shortcut functions).
Currently, the builder provides two options, either or .
In case you are using an IDE, you do not really have to think too much -- use code completion;
the fluent builders will guide you through your decision-making 😊
Following on the last section we will start with an inOrder example:
expect(listOf(1, 2, 2, 4)).toContain.inOrder.only.entries({ toBeLessThan(3) }, { toBeLessThan(2) })
↑ Example ↓ Output
I expected subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>)
◆ ▶ size: 4 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
◆ to contain only, in order:
✔ ▶ element 0: 1 (kotlin.Int <1234789>)
◾ to be less than: 3 (kotlin.Int <1234789>)
✘ ▶ element 1: 2 (kotlin.Int <1234789>)
◾ to be less than: 2 (kotlin.Int <1234789>)
❗❗ additional elements detected:
⚬ element 2: 2 (kotlin.Int <1234789>)
⚬ element 3: 4 (kotlin.Int <1234789>)
Since we have chosen the only option, Atrium shows us a summary1 where we see three things:
- Whether a specified -lambda matched (signified by or )
the corresponding element or not (e.g. was and we expected, it )
😍 We are pretty sure you are going to love this feature as well.
Please star Atrium if you like using it.
1 Atrium shows a summary if we expect up to 10 elements, if we expect more elements,
then only failing expectations are shown.
Following one more example for inOrder as well as a few examples for inAnyOrder.
We think explanations are no longer required at this stage.
In case you have a question (no matter about which section), then please turn up in the
atrium Slack channel
(Invite yourself in case you do not have an account yet)
and we happily answer your question there.
expect(listOf(1, 2, 2, 4)).toContain.inOrder.only.values(1, 2, 2, 3, 4)
↑ Example ↓ Output
I expected subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>)
◆ ▶ size: 4 (kotlin.Int <1234789>)
◾ to equal: 5 (kotlin.Int <1234789>)
◆ to contain only, in order:
✔ ▶ element 0: 1 (kotlin.Int <1234789>)
◾ to equal: 1 (kotlin.Int <1234789>)
✔ ▶ element 1: 2 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
✔ ▶ element 2: 2 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
✘ ▶ element 3: 4 (kotlin.Int <1234789>)
◾ to equal: 3 (kotlin.Int <1234789>)
✘ ▶ element 4: ❗❗ hasNext() returned false
» to equal: 4 (kotlin.Int <1234789>)
expect(listOf(1, 2, 2, 4)).toContain.inAnyOrder.atLeast(1).butAtMost(2).entries({ toBeLessThan(3) })
↑ Example ↓ Output
I expected subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>)
◆ to contain, in any order:
⚬ an element which needs:
» to be less than: 3 (kotlin.Int <1234789>)
⚬ ▶ number of such elements: 3
◾ is at most: 2
expect(listOf(1, 2, 2, 4)).toContain.inAnyOrder.only.values(1, 2, 3, 4)
↑ Example ↓ Output
I expected subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>)
◆ to contain only, in any order:
✔ an element which equals: 1 (kotlin.Int <1234789>)
✔ an element which equals: 2 (kotlin.Int <1234789>)
✘ an element which equals: 3 (kotlin.Int <1234789>)
✔ an element which equals: 4 (kotlin.Int <1234789>)
❗❗ following elements were mismatched:
⚬ 2 (kotlin.Int <1234789>)
expect(listOf(1, 2, 2, 4)).toContain.inAnyOrder.only.values(4, 3, 2, 2, 1)
↑ Example ↓ Output
I expected subject: [1, 2, 2, 4] (java.util.Arrays.ArrayList <1234789>)
◆ ▶ size: 4 (kotlin.Int <1234789>)
◾ to equal: 5 (kotlin.Int <1234789>)
◆ to contain only, in any order:
✔ an element which equals: 4 (kotlin.Int <1234789>)
✘ an element which equals: 3 (kotlin.Int <1234789>)
✔ an element which equals: 2 (kotlin.Int <1234789>)
✔ an element which equals: 2 (kotlin.Int <1234789>)
✔ an element which equals: 1 (kotlin.Int <1234789>)
Map Expectations
Map expectations are kind of very similar to Collection Expectations, also regarding reporting.
That is the reason why we are not going into too much detail here because we assume you are already familiar with it.
We provide again Shortcut Functions for the most common scenarios
and more Sophisticated Expectation Builder for the other cases.
Shortcut Functions
See also
MapExpectationSamples
for further examples.
expect(mapOf("a" to 1, "b" to 2)).toContain("c" to 2, "a" to 1, "b" to 1)
↑ Example ↓ Output
I expected subject: {a=1, b=2} (java.util.LinkedHashMap <1234789>)
◆ to contain, in any order:
⚬ ▶ entry "c": ❗❗ key does not exist
» to equal: 2 (kotlin.Int <1234789>)
⚬ ▶ entry "b": 2 (kotlin.Int <1234789>)
◾ to equal: 1 (kotlin.Int <1234789>)
Next to postulate expectations based on key-value Pairs one can also define sub expectations for the value of
an entry with the help of the parameter object KeyValue:
expect(mapOf("a" to 1, "b" to 2)).toContain(
KeyValue("c") { toEqual(2) },
KeyValue("a") { toBeGreaterThan(2) },
KeyValue("b") { toBeLessThan(2) }
)
↑ Example ↓ Output
I expected subject: {a=1, b=2} (java.util.LinkedHashMap <1234789>)
◆ to contain, in any order:
⚬ ▶ entry "c": ❗❗ key does not exist
» to equal: 2 (kotlin.Int <1234789>)
⚬ ▶ entry "a": 1 (kotlin.Int <1234789>)
◾ to be greater than: 2 (kotlin.Int <1234789>)
⚬ ▶ entry "b": 2 (kotlin.Int <1234789>)
◾ to be less than: 2 (kotlin.Int <1234789>)
In case you expect that a map only contains certain entries, then you can use the shortcut toContainOnly.
Again both overloads are provided, one for key-value Pairs:
expect(mapOf("a" to 1, "b" to 2)).toContainOnly("b" to 2)
↑ Example ↓ Output
I expected subject: {a=1, b=2} (java.util.LinkedHashMap <1234789>)
◆ ▶ size: 2 (kotlin.Int <1234789>)
◾ to equal: 1 (kotlin.Int <1234789>)
◆ to contain only, in any order:
✔ ▶ entry "b": 2 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
❗❗ additional entries detected:
⚬ entry "a": 1 (kotlin.Int <1234789>)
And the other overload which expects a KeyValue and allows defining sub expectations for the value:
expect(mapOf("a" to 1, "b" to 2)).toContainOnly(
KeyValue("c") { toEqual(2) },
KeyValue("a") { toBeLessThan(2) },
KeyValue("b") { toBeLessThan(2) }
)
↑ Example ↓ Output
I expected subject: {a=1, b=2} (java.util.LinkedHashMap <1234789>)
◆ ▶ size: 2 (kotlin.Int <1234789>)
◾ to equal: 3 (kotlin.Int <1234789>)
◆ to contain only, in any order:
✘ ▶ entry "c": ❗❗ key does not exist
» to equal: 2 (kotlin.Int <1234789>)
✔ ▶ entry "a": 1 (kotlin.Int <1234789>)
◾ to be less than: 2 (kotlin.Int <1234789>)
✘ ▶ entry "b": 2 (kotlin.Int <1234789>)
◾ to be less than: 2 (kotlin.Int <1234789>)
Sophisticated Expectation Builders
Most functionality for Map.toContain are provided as shortcut functions but there is a handy one
in case you deal with ordered Maps: .toContain.inOrder.only
There are multiple methods finalising the building process : entry/entries/entriesOf where entry and
again provide two overloads, one expecting key-value s:
expect(mapOf("a" to 1, "b" to 2)).toContain.inOrder.only.entries("b" to 2, "a" to 1)
↑ Example ↓ Output
I expected subject: {a=1, b=2} (java.util.LinkedHashMap <1234789>)
◆ to contain only, in order:
✘ ▶ element 0: a=1 (java.util.LinkedHashMap.Entry <1234789>)
◾ ▶ key: "a" <1234789>
◾ to equal: "b" <1234789>
◾ ▶ value: 1 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
✘ ▶ element 1: b=2 (java.util.LinkedHashMap.Entry <1234789>)
◾ ▶ key: "b" <1234789>
◾ to equal: "a" <1234789>
◾ ▶ value: 2 (kotlin.Int <1234789>)
◾ to equal: 1 (kotlin.Int <1234789>)
And the other expecting KeyValues which allow specifying sub expectations for the value
expect(mapOf("a" to 1, "b" to 2)).toContain.inOrder.only.entries(
KeyValue("a") { toBeLessThan(2) },
KeyValue("b") { toBeLessThan(2) })
↑ Example ↓ Output
I expected subject: {a=1, b=2} (java.util.LinkedHashMap <1234789>)
◆ to contain only, in order:
✔ ▶ element 0: a=1 (java.util.LinkedHashMap.Entry <1234789>)
◾ ▶ key: "a" <1234789>
◾ to equal: "a" <1234789>
◾ ▶ value: 1 (kotlin.Int <1234789>)
◾ to be less than: 2 (kotlin.Int <1234789>)
✘ ▶ element 1: b=2 (java.util.LinkedHashMap.Entry <1234789>)
◾ ▶ key: "b" <1234789>
◾ to equal: "b" <1234789>
◾ ▶ value: 2 (kotlin.Int <1234789>)
◾ to be less than: 2 (kotlin.Int <1234789>)
Others
In case you want to postulate an expectation about a value of one particular key, then you can use getExisting.
For instance:
data class Person(val firstName: String, val lastName: String, val age: Int)
val bernstein = Person("Leonard", "Bernstein", 50)
expect(mapOf("bernstein" to bernstein))
.getExisting() {
feature { f(it::firstName) }.toEqual()
feature { f(it::age) }.toEqual()
}
.getExisting() {
feature { f(it::firstName) }.toEqual()
}
↑ Example ↓ Output
I expected subject: {bernstein=Person(firstName=Leonard, lastName=Bernstein, age=50)} (java.util.Collections.SingletonMap <1234789>)
◆ ▶ get("bernstein"): Person(firstName=Leonard, lastName=Bernstein, age=50) (readme.examples.MapExamples.Person <1234789>)
◾ ▶ age: 50 (kotlin.Int <1234789>)
◾ to equal: 60 (kotlin.Int <1234789>)
In case you have only expectations about the keys or values of the Map then you can use keys or values:
expect(mapOf("a" to 1, "b" to 2)) {
keys { toHaveElementsAndAll { toStartWith("a") } }
values { toHaveElementsAndNone { toBeGreaterThan(1) } }
}
↑ Example ↓ Output
I expected subject: {a=1, b=2} (java.util.LinkedHashMap <1234789>)
◆ ▶ keys: [a, b] (java.util.LinkedHashMap.LinkedKeySet <1234789>)
◾ elements need all:
» to start with: "a" <1234789>
❗❗ following elements were mismatched:
⚬ index 1: "b" <1234789>
◆ ▶ values: [1, 2] (java.util.LinkedHashMap.LinkedValues <1234789>)
◾ not to contain:
⚬ an element which needs:
» to be greater than: 1 (kotlin.Int <1234789>)
❗❗ following elements were mismatched:
⚬ index 1: 2 (kotlin.Int <1234789>)
Last but not least, you can use the non-reporting asEntries() function which
turns Expect<Map<K, V>> into an Expect<Set<Map.Entry<K, V>> and thus allows that you can use all the expectation
functions and sophisticated builders shown in Collection Expectations.
There should seldom be a need for it but in case you want to make also sub expectations for the key,
then it will come in handy:
expect(linkedMapOf("a" to 1, "b" to 2)).asEntries().toContain.inOrder.only.entries(
{ toEqualKeyValue("a", 1) },
{
key.toStartWith("a")
value.toBeGreaterThan(2)
}
)
↑ Example ↓ Output
I expected subject: {a=1, b=2} (java.util.LinkedHashMap <1234789>)
◆ to contain only, in order:
✔ ▶ element 0: a=1 (java.util.LinkedHashMap.Entry <1234789>)
◾ ▶ key: "a" <1234789>
◾ to equal: "a" <1234789>
◾ ▶ value: 1 (kotlin.Int <1234789>)
◾ to equal: 1 (kotlin.Int <1234789>)
✘ ▶ element 1: b=2 (java.util.LinkedHashMap.Entry <1234789>)
◾ ▶ key: "b" <1234789>
◾ to start with: "a" <1234789>
◾ ▶ value: 2 (kotlin.Int <1234789>)
◾ to be greater than: 2 (kotlin.Int <1234789>)
toEqualKeyValue as well as key and value are expectation functions defined for Map.Entry<K, V>.
There are more expectation functions, a full list can be found in
KDoc of atrium-api-fluent.
Path Expectations
See also
PathExpectationSamples
and
PathFeatureExtractorSamples
for further examples.
Atrium’s expectation functions for paths give detailed failure hints explaining what happened on the file system.
For example, toExist will explain which entry was the first one missing:
expect(Paths.get("/usr/bin/noprogram")).toExist()
↑ Example ↓ Output
I expected subject: /usr/bin/noprogram (sun.nio.fs.UnixPath <1234789>)
◆ to: exist
» the closest existing parent directory is /usr/bin
Atrium will give details about why something cannot be accessed, for example when checking whether a file is writable:
expect(Paths.get("/root/.ssh/config")).toBeWritable()
↑ Example ↓ Output
I expected subject: /root/.ssh/config (sun.nio.fs.UnixPath <1234789>)
◆ to be: writable
» failure at parent path: /root (sun.nio.fs.UnixPath <1234789>)
» access was denied
» the owner is root, the group is root
» the permissions are u=rwx g= o=
Even in more complicated scenarios, Atrium explains step by step what happened:
val directory = Files.createDirectory(tmpDir)
val file = Files.createFile(directory.resolve("file"))
val filePointer = Files.createSymbolicLink(directory.resolve("directory"), file)
expect(filePointer.resolve("subfolder/file")).toBeARegularFile()
↑ Example ↓ Output
I expected subject: /tmp/atrium-path/directory/subfolder/file (sun.nio.fs.UnixPath <1234789>)
◆ to be: a file
» followed the symbolic link /tmp/atrium-path/directory to /tmp/atrium-path/file
» failure at parent path: /tmp/atrium-path/file (sun.nio.fs.UnixPath <1234789>)
» was a file instead of a directory
Attaching a Reason
In case you want to add further information to an expectation, e.g. state the reason why you expect it to hold, you can
use because:
expect("filename?")
.because("? is not allowed in file names on Windows") {
notToContain("?")
}
↑ Example ↓ Output
I expected subject: "filename?" <1234789>
◆ not to contain:
⚬ value: "?" <1234789>
⚬ ▶ number of matches: 1
◾ to equal: 0 (kotlin.Int <1234789>)
ℹ because: ? is not allowed in file names on Windows
Integrate other Assertion/Expectation Libraries
If you are in the situation where you either want to migrate a large number of own assertion functions written for a
third party assertion library (e.g. AssertJ) to Atrium or where you want to integrate an assertion library into
the reporting of Atrium, the expectation function toHoldThirdPartyExpectation comes in handy.
It basically allows you to carry out any (expectation) functionality and give it a description and representation in
reporting. The third party expectation is considered to hold if no exception is thrown and to fail otherwise.
Following an example:
expect(listOf(1, 2, 3, -1)).toHaveElementsAndAll {
toHoldThirdPartyExpectation("not to be", Text("negative")) { subject ->
assertThat(subject).isNotNegative()
}
}
↑ Example ↓ Output
I expected subject: [1, 2, 3, -1] (java.util.Arrays.ArrayList <1234789>)
◆ elements need all:
» not to be: negative
❗❗ following elements were mismatched:
⚬ index 3: -1 (kotlin.Int <1234789>)
Please, open a feature request
for features you miss in Atrium. We happily add further functionality as long as it is used by someone.
If you should use this third party expectation often, then it makes sense to write an own expectation function:
fun <T : Number> Expect. =
toHoldThirdPartyExpectation(, Text()) { subject ->
(subject) {
-> assertThat(subject).isNotNegative()
-> assertThat(subject).isNotNegative()
-> assertThat(subject).isNotNegative()
-> assertThat(subject).isNotNegative()
BigDecimal -> assertThat(subject).isNotNegative()
-> assertThat(subject.toDouble()).isNotNegative()
}
}
(-).notToBeNegative()
↑ Example ↓ Output
I expected subject: -10 (kotlin.Int <1234789>)
◆ not to be: negative
ℹ Properties of the unexpected AssertionError
» message: "
Expecting actual:
-10
to be greater than or equal to:
0
" <1234789>
» stacktrace:
⚬ readme.examples.ThirdPartyExamples$ex-third-party-2$notToBeNegative$1.invoke(ThirdPartyExamples.kt:38)
⚬ readme.examples.ThirdPartyExamples$ex-third-party-2$notToBeNegative$1.invoke(ThirdPartyExamples.kt:36)
⚬ readme.examples.ThirdPartyExamples.ex_third_party_2$notToBeNegative(ThirdPartyExamples.kt:36)
⚬ readme.examples.ThirdPartyExamples.ex-third-party-2(ThirdPartyExamples.kt:48)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
⚬ java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
⚬ java.base/java.lang.reflect.Method.invoke(Method.java:566)
As you can see, in case of failure we see our defined description and representation as well as properties of the
thrown Exception. Of course, if we start writing that much code, it might also be easier to just migrate it to
Atrium (and create a PR so that others benefit as well 😉):
↑ Example ↓ Output
I expected subject: -10 (kotlin.Int <1234789>)
◆ not to be: negative
Data Driven Testing
See also
GroupingSamples
for further examples.
Atrium is not intended for data driven testing in the narrowed sense in terms that it cannot produce multiple tests.
This is the responsibility of your test runner.
However, Atrium let you define multiple expectations within one test and reports them all if you want.
In this sense it can be used for data driven testing.
This is especially helpful in case your test runner does not support data driven testing (or other mechanisms like hierarchical or dynamic tests).
As an example, Atrium can help you to write data driven tests in a common module of a multiplatform-project.
Use expectGrouped (a pre-defined expectation verb which ships along with expect) instead and then define multiple
expect in it. Following an example:
fun myFun(i: Int) = (i + 97).toChar()
expectGrouped {
mapOf(
1 to 'a',
2 to 'c',
3 to 'e'
).forEach { (arg, result) ->
group("calling myFun with $arg") {
expect(myFun(arg)).toEqual(result)
}
}
}
↑ Example ↓ Output
my expectations:
# calling myFun with 1:
◆ ▶ I expected subject: 'b'
◾ to equal: 'a'
# calling myFun with 3:
◆ ▶ I expected subject: 'd'
◾ to equal: 'e'
Per default, only failing expectations are shown.
This is also the reason why the call of myFun(2) is not listed (as the result is c as expected).
expectGrouped creates an ExpectGrouping-Block which is very similar to an expectation-group block
(see Define an expectation-group) just that you have not yet
defined a subject. It also specifies that all expectations specified in it are evaluated and reported together
and this is also the reason why we see calling myFun with 3 in the above Output even though
calling it with 1 failed.
Please create a feature request
if you want to see a summary, meaning also successful expectations -- we happily add more functionality if it is of use for someone.
Following another example which involves an assertionCreator-lambda and not only a simple toEqual check.
We are going to reuse the myFun from above:
expectGrouped {
mapOf<Int, ExpectationCreator<Char>>(
1 to { toBeLessThan('f') },
2 to { toEqual('c') },
3 to { toBeGreaterThan('e') }
).forEach { (arg, assertionCreator) ->
group("calling myFun with $arg") {
expect(myFun(arg), assertionCreator)
}
}
}
↑ Example ↓ Output
my expectations:
# calling myFun with 3:
◆ ▶ I expected subject: 'd'
◾ to be greater than: 'e'
The example should be self-explanatory.
One detail to note though is the usage of ExpectationCreator.
It's a typealias for Expect<T>.() -> Unit and reduces some verbosity. Its usage is of course optional.
In case you should run into type inference issues, then prepend your lambda with expectLambda
(for instance expectLambda { toBeLessThan('f') }), it's a helper function which gives Kotlin an additional hint.
So far we have not shown it but you can also nest groups and even use groups within expect. For instance:
val x1 = 1
val x2 = 3
val y = 6
expectGrouped {
group("first group") {
expect(x1).toEqual(2)
group("sub-group") {
(x2).toBeGreaterThan()
}
}
group() {
(y) {
group() {
toBeGreaterThan()
toBeLessThan()
}
group() {
notToEqual()
}
}
}
}
↑ Example ↓ Output
my expectations:
# first group:
◆ ▶ I expected subject: 1 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
# sub-group:
◆ ▶ I expected subject: 3 (kotlin.Int <1234789>)
◾ to be greater than: 5 (kotlin.Int <1234789>)
# second group:
◆ ▶ I expected subject: 6 (kotlin.Int <1234789>)
# sub-group 1:
◆ to be less than: 5 (kotlin.Int <1234789>)
# sub-group 2:
◆ not to equal: 6 (kotlin.Int <1234789>)
There is another function worth mentioning here which comes in handy in data-driven testing in case the subject has a
nullable type
If you wish to make sub expectations on the non-nullable type of the subject, then you can use
toEqualNullIfNullGivenElse which accepts an assertionCreator-lambda or null.
It is short for if (assertionCreatorOrNull == null) toEqual(null) else notToEqual(assertionCreatorOrNull).
Following another fictional example which illustrates toEqualNullIfNullGivenElse (we are reusing myFun from above):
= (i > ) i.toString()
expectGrouped {
mapOf<, ExpectationCreator<String>?>(
.MIN_VALUE to { toContain() },
- to ,
to ,
to { toEqual() },
to { toEndWith() },
.MAX_VALUE to { toEqual() }
).forEach { (arg, assertionCreatorOrNull) ->
group() {
(myNullableFun(arg)).toEqualNullIfNullGivenElse(assertionCreatorOrNull)
}
}
}
↑ Example ↓ Output
my expectations:
# calling myFun with -2147483648:
◆ ▶ I expected subject: null
» to contain:
⚬ value: "min" <1234789>
» but no match was found
# calling myFun with 2147483647:
◆ ▶ I expected subject: "2147483647" <1234789>
◾ to equal: "max" <1234789>
Further Examples
Atrium supports further expectation builders (e.g, for CharSequence)
as well as expectation functions which have not been shown in the examples above.
Take a look at the sample files which are used i.a. in the KDOC of the corresponding expectation functions:
How is Atrium different from other Expectation/Assertion Libraries
The following subsections shall give you a quick overview how Atrium differs from other assertion libraries.
Ready to Help
Atrium is designed to help you whenever possible.
We think this is the biggest difference to other expectation libraries and a very handy one indeed.
1. Fluent API with Code Documentation
Atrium provides a fluent API where the design focus was put on the interoperability (of the API)
with the code completion functionality of your IDE.
Or in other words, you can always use code completion to get direct help from your IDE.
This experience is improved by providing up-to-date code documentation (in form of KDoc)
for all expectation functions, including samples, so that you get the extra help needed.
2. Additional Information in Failure Reporting
Atrium adds extra information to error messages so that you get quickly a better idea of what went wrong.
For instance, for the following expectation (which fails):
expect(listOf(1, 2, 3)).toContain.inOrder.only.values(1, 3)
Atrium points out which values were found, makes an implicit expectation about the size and
also states which entries were additionally contained in the list:
I expected subject: [1, 2, 3] (java.util.Arrays.ArrayList <1234789>)
◆ ▶ size: 3 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
◆ to contain only, in order:
✔ ▶ element 0: 1 (kotlin.Int <1234789>)
◾ to equal: 1 (kotlin.Int <1234789>)
✘ ▶ element 1: 2 (kotlin.Int <1234789>)
◾ to equal: 3 (kotlin.Int <1234789>)
❗❗ additional elements detected:
⚬ element 2: 3 (kotlin.Int <1234789>)
Let us have a look at another example.
expect(9.99f).toEqualWithErrorTolerance(10.0f, 0.01f)
The above expectation looks good at first sight but actually fails (at least on @robstoll's machine).
And without some extra information in the output we would believe that there is actually a bug in the expectation library itself.
But Atrium shows where it goes wrong and even gives a possible hint:
I expected subject: 9.99 (kotlin.Float <1234789>)
◆ to equal (error ± 0.01): 10.0 (kotlin.Float <1234789>)
» failure might be due to using kotlin.Float, see exact check on the next line
» exact check was |9.989999771118164 - 10.0| = 0.010000228881835938 ≤ 0.009999999776482582
One last example. This time about formulating an expectation that a certain Throwable is thrown but
the expectation fails because it was the wrong one.
Atrium comes with a very useful hint, it shows the actual exception:
expect {
try {
throw UnsupportedOperationException("not supported")
} catch (t: Throwable) {
throw IllegalArgumentException("no no no...", t)
}
}.toThrow<IllegalStateException> { messageToContain("no no no") }
↑ Example ↓ Output
I expected subject: () -> kotlin.Nothing (readme.examples.MostExamples$ex-add-info-3$1 <1234789>)
◆ ▶ thrown exception when called: java.lang.IllegalArgumentException
◾ to be an instance of type: IllegalStateException (java.lang.IllegalStateException)
» ▶ message:
◾ not to equal: null but to be an instance of: String (kotlin.String) -- Class: java.lang.String
◾ to contain:
⚬ value: "no no no" <1234789>
» but no match was found
ℹ Properties of the unexpected IllegalArgumentException
» message: "no no no..." <1234789>
» stacktrace:
⚬ readme.examples.MostExamples$ex-add-info-3$1.invoke(MostExamples.kt:117)
⚬ readme.examples.MostExamples$ex-add-info-3$1.invoke(MostExamples.kt:113)
⚬ readme.examples.MostExamples.ex-add-info-3(MostExamples.kt:148)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
⚬ java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
⚬ java.base/java.lang.reflect.Method.invoke(Method.java:566)
» cause: java.lang.UnsupportedOperationException
» message: "not supported" <1234789>
» stacktrace:
⚬ readme.examples.MostExamples$ex-add-info-3$1.invoke(MostExamples.kt:115)
3. Prevents you from Pitfalls
But not enough. There are certain pitfalls when it comes to using an expectation library and Atrium tries to prevent you from those.
For instance, an overload of and of for was introduced which are both deprecated and throw a .
The reason behind it?
It is very likely that a user actually wants to compare that a certain is numerically (not) equal to another
rather than including in the comparison.
Accordingly, the deprecation message of ( alike) explains the problem and suggests to either use or .
And if the user should decide to use and at some point an expectation fails only due to the comparison of
then Atrium reminds us of the possible pitfall. For instance:
expect(BigDecimal.TEN).toEqualIncludingScale(BigDecimal("10.0"))
↑ Example ↓ Output
I expected subject: 10 (java.math.BigDecimal <1234789>)
◆ is equal (including scale): 10.0 (java.math.BigDecimal <1234789>)
💡 notice, if you used toEqualNumerically then the expectation would have been met.
Another example are empty assertionCreator-lambdas.
Getting distracted by a working colleague and taking up the work at the wrong position might sound familiar to you.
For instance:
expect(listOf(1)).get(0) {}
↑ Example ↓ Output
I expected subject: [1] (java.util.Collections.SingletonList <1234789>)
◆ ▶ get(0): 1 (kotlin.Int <1234789>)
◾ at least one expectation defined: false
» You forgot to define expectations in the assertionCreator-lambda
» Sometimes you can use an alternative to `{ }` For instance, instead of `toThrow<..> { }` you should use `toThrow<..>()`
Flexibility
Another design goal of Atrium was to give you the flexibility needed but still adhere to a concise design.
First and most importantly, Atrium does not enforce a certain style on your code base.
Quite the contrary, it gives you the flexibility to choose a desired name for the expectation verb,
it continues by providing the possibility to configure the reporting style,
goes on that you can choose from different API Styles
and ends that you can replace almost all components by other implementations and hook into existing.
So for instance, if you like to use an infix API, then use the bundle atrium-infix.
You prefer pure fluent and do not even want to see infix style in your code,
then use atrium-fluent which provides a pure fluent style API.
You are free to choose what fits best without introducing ambiguity etc.
You could even mix up different API-styles if needed (but not without losing conciseness -- but hey, it is your decision 😉).
Migration of Deprecated Functionality
Atrium follows Semantic Versioning and tries to be binary backward compatible within a major version (since 0.6.0).
Until 2.0.0 this is only true for the API level, we reserve the right to break things on the logic and core level until then.
Moreover, we follow the principle that a user of Atrium has enough time to migrate its code to new functionality before a next major version.
We provide this in form of @Deprecated annotations with a corresponding ReplaceWith
as well as migration guides in the Release Notes.
This way we hope that we provide a pleasant way to stay up-to-date without the need to migrate everything from one day to the other.
Write own Expectation Functions
Are you missing an expectation function for a specific type and the generic
Feature Extractors are not good enough?
The following subsections will show how you can write your own expectation functions.
A pull request of your new expectation function is very much appreciated.
Boolean based Expectation Functions
This is kind of the simplest way of defining expectation functions. Following an example:
import ch.tutteli.atrium.logic._logic
fun Expect<Int>.toBeAMultipleOf(base: Int) =
_logic.createAndAppend("is multiple of", base) { it % base == 0 }
and its usage:
expect(12).toBeAMultipleOf(5)
↑ Example ↓ Output
I expected subject: 12 (kotlin.Int <1234789>)
◆ is multiple of: 5 (kotlin.Int <1234789>)
Let us see how we actually defined toBeAMultipleOf.
In most cases you probably use the expected value itself as its representation -- so you pass it as second argument.
And finally you specify the test as such in the lambda passed as third argument.
But not all expectation functions require a value which is somehow compared against the subject
-- some state an expectation about a characteristic of the subject without comparing it against an expected value.
Consider the following expectation function:
import ch.tutteli.atrium.logic._logic
fun Expect<Int>.toBeEven() =
_logic.createAndAppend("is", Text("an even number")) { it % 2 == 0 }
We are using a Text as
representation so that "an even number" is not treated as a String in reporting.
Its usage looks then as follows:
expect(13).toBeEven()
↑ Example ↓ Output
I expected subject: 13 (kotlin.Int <1234789>)
◆ is: an even number
Throwable based expectation functions
You might already implement functions (e.g. in your business code) which check/validate certain things and throw if
the expectations are not met. If those functions are well tested by itself, then they can very well also act as
expectation functions in other tests. To integrate those functions into the reporting of Atrium you can use
toHoldThirdPartyExpectation. Take a look at Integrate other Assertion/Expectation Libraries
for a first explanation. Assuming, that are you going to use your existing functionality more than once in tests,
an expectation function could look as follows:
fun Expect<MyDomainModel>.toComplyValidation() =
toHoldThirdPartyExpectation("to comply", Text("validation")) { subject ->
subject.validateMinThreshold()
subject.validateMaxThreshold()
}
expect(MyDomainModel(alpha1 = 1204)).toComplyValidation()
↑ Example ↓ Output
I expected subject: MyDomainModel(alpha1=1204) (readme.examples.ThirdPartyExamples.MyDomainModel <1234789>)
◆ to comply: validation
ℹ Properties of the unexpected IllegalStateException
» message: "threshold value for alpha1 exceeded, expected <= 1000, was 1204" <1234789>
» stacktrace:
⚬ readme.examples.ThirdPartyExamples.validateMaxThreshold(ThirdPartyExamples.kt:75)
⚬ readme.examples.ThirdPartyExamples$ex-third-party-10$toComplyValidation$1.invoke(ThirdPartyExamples.kt:83)
⚬ readme.examples.ThirdPartyExamples$ex-third-party-10$toComplyValidation$1.invoke(ThirdPartyExamples.kt:81)
⚬ readme.examples.ThirdPartyExamples.ex_third_party_10$toComplyValidation(ThirdPartyExamples.kt:81)
⚬ readme.examples.ThirdPartyExamples.ex-third-party-10(ThirdPartyExamples.kt:87)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
⚬ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
⚬ java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
⚬ java.base/java.lang.reflect.Method.invoke(Method.java:566)
Compose Expectation Functions
So far, we core contributors ran quickly into the situation where we wanted to compose functions or
reuse existing functions but with different arguments.
We will show both use cases here, starting off by composing functions.
Say you want to build a toBeBetween expectation function for java.util.Date, you could write it as follows:
fun <T : Date> Expect<T>.toBeBetween(lowerBoundInclusive: T, upperBoundExclusive: T) =
and {
toBeGreaterThanOrEqualTo(lowerBoundInclusive)
toBeLessThan(upperBoundExclusive)
}
Pretty simple, isn't it?
Note, using and {...} creates an expectation group-block
and therefore both toBeGreaterThanOrEqualTo and toBeLessThan are evaluated and reported.
If wou prefer a fail-fast behaviour then you could write it as follows but from our experience more context in error
messages ways more than a tiny bit faster test execution stop:
toBeGreaterThanOrEqualTo(lowerBoundInclusive).and.toBeLessThan(upperBoundExclusive)
So let's move on to an example which is a bit more complicated. Assume the following data class Person
data class Person(
val firstName: String,
val lastName: String,
val age: Int,
val children: Collection<Person>
)
Say you want to postulate an expectation about the number of children a person has:
fun Expect<Person>.toHaveNumberOfChildren(number: Int): Expect<Person> =
feature(Person::children) { toHaveSize(number) }
Three things to notice here:
- we make use of a feature extractor with class reference.
- We use the overload which expects an
assertionCreator-lambda. This way subsequent expectations are still made on Person and not on children.
- We have not used a type parameter in contrast to the previous example, because Person is final and doesn't have type
parameters by its own. If it were open, we would again use so
that this expectation function is also available on subtypes of Person.
Its usage is then as follows:
expect(Person("Susanne", "Whitley", 43, emptyList()))
.toHaveNumberOfChildren(2)
↑ Example ↓ Output
I expected subject: Person(firstName=Susanne, lastName=Whitley, age=43, children=[]) (readme.examples.Person <1234789>)
◆ ▶ children: [] (kotlin.collections.EmptyList <1234789>)
◾ ▶ size: 0 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
Another example: expect the person to have children which are all adults (assuming 18 is the age of majority).
fun Expect<Person>.toHaveAdultChildren(): Expect<Person> =
feature(Person::children) {
toHaveElementsAndAll {
feature(Person::age).toBeGreaterThanOrEqualTo(18)
}
}
We once again use feature with an expectation-group
for the same reason as above.
Note how toHaveElementsAndAll already checks that there is at least one element.
I.e. it fails for a Person with 0 children, because such a person does not have adult children.
expect(Person("Susanne", "Whitley", 43, emptyList()))
.toHaveAdultChildren()
↑ Example ↓ Output
I expected subject: Person(firstName=Susanne, lastName=Whitley, age=43, children=[]) (readme.examples.Person <1234789>)
◆ ▶ children: [] (kotlin.collections.EmptyList <1234789>)
◾ to have: a next element
» elements need all:
» ▶ age:
◾ to be greater than or equal to: 18 (kotlin.Int <1234789>)
If we keep adding expectation functions involving children it might be best to provide a shortcut property and function.
val Expect<Person>.children: Expect<Collection<Person>> get() = feature(Person::children)
fun Expect<Person>.children(assertionCreator: Expect<Collection<Person>>.() -> Unit): Expect<Person> =
feature(Person::children, assertionCreator)
Notice, that we have used a class-reference and not a bounded-reference to refer to children which is best practice
(see feature extractor within expectation functions).
With this, we can write things like:
expect(Person("Susanne", "Whitley", 43, listOf(Person("Petra", "Whitley", 12, emptyList()))))
.children {
toHaveElementsAndNone {
feature { f(it::firstName) }.toStartWith("Ro")
}
toHaveElementsAndAll {
feature { f(it::lastName) }.toEqual("Whitley")
}
}
.children
.toHaveSize()
.toHaveElementsAndAny {
feature { f(it::age) }.toBeGreaterThan()
}
↑ Example ↓ Output
I expected subject: Person(firstName=Susanne, lastName=Whitley, age=43, children=[Person(firstName=Petra, lastName=Whitley, age=12, children=[])]) (readme.examples.Person <1234789>)
◆ ▶ children: [Person(firstName=Petra, lastName=Whitley, age=12, children=[])] (java.util.Collections.SingletonList <1234789>)
◾ ▶ size: 1 (kotlin.Int <1234789>)
◾ to equal: 2 (kotlin.Int <1234789>)
Enough of expectation functions for features. Let's move on to an example where we want to reuse an existing function
but with different arguments. Say we have a function which returns a list of first name / last name s.
We want to assert that the pairs contain only the first name / last name pairs of certain s in any order.
will help us with this.
However, expects s.
So we have to map from to upfront.
As we have a variable length argument list and want to pass it to a variable length argument list, this cannot be done with a simple from Kotlin.
And it gets worse if we want to use which expects at least one -lambda ()
because Kotlin cannot infer the types automatically.
mapArguments to the rescue, you can write the expectation function as follows:
import ch.tutteli.atrium.logic.utils.mapArguments
fun <T : List<Pair<String, String>>> Expect<T>.areNamesOf(
person: Person, vararg otherPersons: Person
): Expect<T> {
val (pair, otherPairs) = mapArguments(person, otherPersons) { it.firstName to it.lastName }
return toContain.inAnyOrder.only.values(pair, *otherPairs)
}
As you can see we moved the mapping inside the function so that the consumer of our API can happily use it as follows:
expect(get...WhichReturnsPairs()).areNamesOf(fKafka, eBloch, kTucholsky)
Another fictional example, say we expect that the pairs have the same initials as the given persons and in the given order.
Which means, this time we need to use assertionCreator-lambdas. This can be written as follows:
fun <T : List<Pair<String, String>>> Expect<T>.sameInitialsAs(
person: Person, vararg otherPersons: Person
): Expect<T> {
val (first, others) = mapArguments(person, otherPersons).toExpect<Pair<String, String>> {
first.toStartWith(it.firstName[0].toString())
second.toStartWith(it.lastName[0].toString())
}
return toContain.inOrder.only.entries(first, *others)
}
There are a few additional methods which you can call after mapArguments.
See KDoc of ArgumentMapperBuilder.
In case you want to provide your own implementation,
it suffices to create an extension function for ArgumentMapperBuilder.
Enhanced Reporting
Composing expectation functions gives already quite a bit of power to an expectation function writer.
Yet, sometimes we would like to create functions which have a better error reporting than the one we get
when we compose expectation functions.
_logic
is the entry point to AssertionContainer which is the equivalent of Expect but on a lower level.
Following a quick overview what extension methods could be useful:
Besides, the assertionBuilder allows to create different kinds of Assertions
(see AssertionBuilder for more information)
which can be used to create very specific expectation functions.
You can find an example in floatingPointAssertions
which makes use of explanatory assertions as well as providing a failure hint.
Unfortunately we do not have the time to cover all cases, so let us know if you want to know more
-- either by opening an issue or via the atrium Slack channel
(Invite yourself).
Own Sophisticated Expectation Builders
Do you want to write an own sophisticated expectation builder (or extend a current one with more options) instead of an expectation function?
Great, we do not provide documentation yet (had only one question about it since 2017).
… truncated for performance. View the full file on GitHub.