1. Introduction

In this article, we’re going to have a look at Atrium. We’ll see what it is, what we can do with it, and how to use it.

Atrium is an assertion library for Kotlin that supports JVM, JavaScript, and Android. It allows us to write assertions in a highly fluent style while still being typesafe and extensible. It offers both a fluent API—which we’ll explore in this article—and an infix API.

2. Dependencies

Before using Atrium, we need to include the latest version in our build, which is 1.2.0 at the time of writing.

If we’re using Maven, we can include this dependency:

<dependency>
    <groupId>ch.tutteli.atrium</groupId>
    <artifactId>atrium-fluent-jvm</artifactId>
    <version>1.2.0</version>
</dependency>

Alternatively, if we wish to use the infix API instead of the fluent one, we’ll need to include ch.tutteli.atrium:atrium-infix-jvm instead. Further, if we wish to target JavaScript instead of JVM or Android, we’ll need to include ch.tutteli.atrium:atrium-fluent-js or ch.tutteli.atrium:atrium-infix-js instead.

Or if we’re using Gradle, we can include it like this:

implementation("ch.tutteli.atrium:atrium-fluent:1.2.0")

Note that the dependency is different for Gradle. We don’t need to specify if we’re targeting JVM or JavaScript, and the build will automatically include the correct dependencies.

At this point, we’re ready to start using it in our application.

3. Simple Assertions

Once we’ve got Atrium available for use, we’re ready to use it in our tests.

Before we can use Atrium in our code, we need to ensure that we’ve got the correct imports:

import ch.tutteli.atrium.api.fluent.en_GB.*
import ch.tutteli.atrium.api.verbs.expect

The ch.tutteli.atrium.api.verbs.expect import is our main assertion function, and the ch.tutteli.atrium.api.fluent.en_GB package contains all of our assertions.

Having done this, we can now write assertions:

expect(value).toEqual("Hello")

This is the equivalent to the assertEquals function from JUnit. However, the output is cleaner than from JUnit:

I expected subject: "Hello"        <1047460013>
◆ to equal: "Hello, World!"        <6444850>

Here, the “subject” value is what we passed in to expect(), and the “to equal” value is what we passed into toEqual().

We have a whole range of standard assertions that we can use in this way, for example:

expect(value).toMatch(regex)
expect(value).toBeGreaterThan(min)

These are all typesafe, and the exact assertions we can use depend on the type in question. For example, we don’t even have toMatch(regex) as an option if value isn’t a string.

3.1. Multiple Assertions

Often, we need to perform multiple assertions on the same value. We can do this by simply performing multiple assertions, but we can go a step further and chain all of the assertions together in a single call as needed:

expect(value).toBeGreaterThan(0)
  .toBeLessThan(5)

This will pass if value is greater than 0 and less than 5. If one of those checks fails, then the test will immediately stop there:

I expected subject: -3        (kotlin.Int <261841467>)
◆ to be greater than: 0        (kotlin.Int <1907622760>)

This clearly tells us what’s going on and exactly which assertion has failed.

Alternatively, sometimes we want to run all of the assertions and show every failure, instead of just the first one. We can do this by grouping them in a lambda that’s passed into the expect() call:

expect(value) {
    toStartWith("Hello")
    toEndWith("World")
}

In this case, the receiver in our lambda is the expect() value, allowing us to write fluent assertions within the provided block.

This will then perform every assertion provided and output the results of all the failing ones:

I expected subject: "Wrong"        <1765795529>
◆ to start with: "Hello"        <48208774>
◆ to end with: "World"        <929383713>

Note that only failed assertions are shown. Any assertions we included in our block that passed are omitted for brevity, which helps us quickly identify what’s going on.

3.2. Grouping Expectations

As well as performing multiple assertions on the same subject, we can group different but related expectations together.

If we want to group expectations, we must first use the expectGrouped() function. This takes a lambda that is used to define our groups. Within this lambda, we can then use the group() function to define each of our groups:

expectGrouped {
    group("First group") {
        expect("Hello").toEqual("World")
        expect("Hello").toEqual("Again")
    }
    group("Second group") {
        expect("Hello").toEqual("World")
    }
}

When we run this, every single group is executed regardless of the results of the other groups. Further, within each group, we’ll execute all of the provided expectations instead of failing on the first one – following the same rules that we saw earlier for grouping assertions together:

# First group: 
  ◆ ▶ I expected subject: "Hello"        <1744189907>
      ◾ to equal: "World"        <1809194904>
  ◆ ▶ I expected subject: "Hello"        <1744189907>
      ◾ to equal: "Again"        <1219273867>
# Second group: 
  ◆ ▶ I expected subject: "Hello"        <1744189907>
      ◾ to equal: "World"        <1809194904>

The way that we define groups can be anything Kotlin allows. This includes generating them within a loop, allowing for a crude form of data-driven testing:

expectGrouped {
    for (i in 1..3) {
        group("Group ${i}") {
            expect(i).toBeLessThan(2)
        }
    }
}

This will then generate groups based on our loop, execute them, and output the results as expected:

my expectations: 
# Group 2: 
  ◆ ▶ I expected subject: 2        (kotlin.Int <2096578823>)
      ◾ to be less than: 2        (kotlin.Int <2096578823>)
# Group 3: 
  ◆ ▶ I expected subject: 3        (kotlin.Int <1070161412>)
      ◾ to be less than: 2        (kotlin.Int <2096578823>)

3.3. Describing Assertions

Often, when we declare assertions, giving a more descriptive reason for them can be helpful. This can help make our output more descriptive and give more understanding to the tests.

We achieve this with the because() call. This takes our description and a lambda that performs the assertions:

expect(value).because("that's the value we expected" ) {
    toEqual("Hello, World!")
}

As before, the receiver of the lambda is our expectation so we can write more fluent calls. If this test fails, then the output will now show:

I expected subject: "Hello"        <423583818>
◆ to equal: "Hello, World!"        <1029472813>
ℹ because: that's the value we expected

Here, we’ve clearly got the value we were checking, the assertion we expected, and our test description.

Note that because we’re passing in the assertions as a lambda, we’re actually using the exact same syntax as above. This means that we can run multiple assertions in the block, and they will all be evaluated. Any failures will be output as one block with the same description.

4. Asserting Properties

So far, we’ve only been asserting on the entire object provided. However, we can also assert on parts of the object—either its properties or the result of methods defined on it.

We achieve this by using the its() method, providing an extractor to get the value from our object, and a lambda to perform the actual assertion:

expect(user)
  .its(User::username) { toEqual("Username") }
  .its(User::displayName) { toEqual("Display Name") }
  .its(User::isEnabled) { toEqual(true) }

In this case, each of the calls to its() is a chained assertion, and so we’ll stop on the first failure:

I expected subject: User(username=baeldung, displayName=Baeldung)        (com.baeldung.atrium.FluentAssertionUnitTest$propertyExtraction$User <1665197552>)
◆ ▶ its.definedIn(FluentAssertionUnitTest.kt:65): "baeldung"        <252480153>
    ◾ to equal: "Username"        <1422238463>

We can instead provide them as a block in the same way as above:

expect(user) {
    its(User::username) { toEqual("Username") }
    its(User::displayName) { toEqual("Display Name") }
    its(User::isEnabled) { toEqual(true) }
}

In this case, all of our properties are asserted, and we’ll see all of the failures:

I expected subject: User(username=baeldung, displayName=Baeldung)        (com.baeldung.atrium.FluentAssertionUnitTest$multiplePropertyExtraction$User <1522095831>)
◆ ▶ its.definedIn(FluentAssertionUnitTest.kt:84): "baeldung"        <2028265136>
    ◾ to equal: "Username"        <1191654595>
◆ ▶ its.definedIn(FluentAssertionUnitTest.kt:85): "Baeldung"        <754177595>
    ◾ to equal: "Display Name"        <1987375157>

Unfortunately, while this is simple enough to use, this output isn’t as clear as it could be. In particular, it doesn’t give us the field that we’re asserting on. We do have an alternative that we can use, though – feature() – which is more complex to write in our code but gives better output:

expect(user) {
    feature({f(it::username)}) { toEqual("Username") }
    feature({f(it::displayName)}) { toEqual("Display Name") }
    feature({f(it::isEnabled)}) { toEqual(true) }
}

In this case, the feature() method takes two lambdas. The first must make a call to the f() method on its receiver, passing in the extractor. The second lambda is then our assertions. This then uses reflection to generate a better failure message for any failing assertions:

I expected subject: User(username=baeldung, displayName=Baeldung)        (com.baeldung.atrium.FluentAssertionUnitTest$multipleFeatureExtraction$User <1965445467>)
◆ ▶ username: "baeldung"        <1042790962>
    ◾ to equal: "Username"        <540325452>
◆ ▶ displayName: "Baeldung"        <1976804832>
    ◾ to equal: "Display Name"        <1959910454>

Here, we can clearly see the failing values and which fields they were extracted from.

5. Type Assertions

In some cases, we need to assert not on the value of something but on the type of it. For example, given a type hierarchy as follows:

open class Animal(val name: String)
class Dog(name: String, val breed: String) : Animal(name)
class Cat(name: String, val color: String) : Animal(name)

We might have an Animal that we want to assert on.

Atrium supports using reified generics to assert the type of a value, which makes our code much cleaner:

expect(animal).toBeAnInstanceOf<Cat>()

This then clearly tells us what we expected and what we actually had:

I expected subject: com.baeldung.atrium.FluentAssertionUnitTest$typeAssertions$Dog@32b260fa        (com.baeldung.atrium.FluentAssertionUnitTest$typeAssertions$Dog <850551034>)
◆ to be an instance of type: Cat (null) -- Class: com.baeldung.atrium.FluentAssertionUnitTest$typeAssertions$Cat

We can also provide a lambda to assert on the values of the type that we expected:

expect(animal).toBeAnInstanceOf<Dog> {
    its(Animal::name).toEqual("Lassie")
    its(Dog::breed).toEqual("Pomeranian")
}

In this case, within our lambda, the value we’re asserting on is now a Dog and not an Animal. That means we can use expectations specific to that type, and the compiler won’t complain. If the value isn’t the correct type then we’ll have failed earlier. However, if our value has the correct type, then the assertions run. We can then observe how they might succeed or fail:

I expected subject: com.baeldung.atrium.FluentAssertionUnitTest$typeAssertions$Dog@121314f7        (com.baeldung.atrium.FluentAssertionUnitTest$typeAssertions$Dog <303240439>)
◆ ▶ its.definedIn(FluentAssertionUnitTest.kt:118): "Rough collie"        <1873091796>
    ◾ to equal: "Pomeranian"        <2095064787>

6. Custom Assertions

One of the things that helps make Atrium stand out above the alternatives is the ease with which we can define custom assertions. All assertions in Atrium are extension functions defined on the Expect class. This allows us to define our extension functions in exactly the same way.

We create the function using the _logic helper functions. The most common way will be to use _logic.createAndAppend, which takes a description of the assertion and lambda to perform the actual test:

fun Expect<Int>.toBeAMultipleOf(base: Int) = 
    _logic.createAndAppend("to be a multiple of", base) { it % base == 0 }

Once we’ve defined this, we can use it exactly like any other assertion. Note that because this is defined on Expect then, it means that the assertion will only be available when the subject we’re checking is itself an Int:

expect(3).toBeAMultipleOf(2)

Unsurprisingly, when this fails, then it will output exactly what we wanted it to, making it obvious what the problem was:

I expected subject: 3        (kotlin.Int <1070161412>)
◆ to be a multiple of: 2        (kotlin.Int <2096578823>)

Alternatively, we can simply define one expectation in terms of another. For example, we could define a new expectation to check if a number is between two others as follows:

fun Expect<Int>.toBeBetween(low: Int, high: Int) = and {
    toBeGreaterThan(low)
    toBeLessThan(high)
}

Doing this is the same as just running the alternative assertion on our subject – in this case, a combination of toBeGreaterThan() and toBeLessThan(). If those fail, then we’ll get the standard output we’d expect from them:

I expected subject: 3        (kotlin.Int <1070161412>)
◆ to be greater than: 5        (kotlin.Int <449308954>)

6.1. Assertions on Custom Types

In addition to defining assertions on standard types, we can leverage the generic nature of Expect<> to define assertions on our own custom types. This allows us to build an assertion library with our domain types. For example, we could have defined some custom assertions for our User type as follows:

fun Expect<User>.toHaveUsername(username: String) = its(User::username) { toEqual(username) }

We can then use this whenever we have a User type that we want to assert on:

expect(user).toHaveUsername("Other")

Doing this will act exactly as expected:

I expected subject: User(username=baeldung, displayName=Baeldung)        (com.baeldung.atrium.FluentAssertionUnitTest$customAssertion$User <477533894>)
◆ ▶ its.definedIn(FluentAssertionUnitTest.kt:169): "baeldung"        <517254671>
    ◾ to equal: "Other"        <1990519794>

7. Summary

Here’s a quick introduction to Atrium. This library can do much more, so why not try it out and see?

All of the examples are available over on GitHub.