Skip to content
forked from robstoll/atrium

A multiplatform assertion library for Kotlin

License

Notifications You must be signed in to change notification settings

angelsupp/atrium

Β 
Β 

Repository files navigation

Download EUPL atrium @ kotlinlang.slack.com Build Status Ubuntu Build Status Windows Coverage Newcomers Welcome

Atrium

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, different error reporting styles and Internationalization (i18n). 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.

Atrium currently provides two API Styles: pure fluent and infix where both of them have their design focus on usability in conjunction with code completion functionality provided by your IDE. See Examples below to get a feel for how you could benefit from Atrium.


❗ You are taking a sneak peek at the next version. Please have a look at the README of the git tag in case you are looking for the documentation of the corresponding version. For instance, the README of v0.18.0.


Table of Content

Installation

JVM

Atrium is published to mavenCentral.

build.gradle.kts:

repositories {
    mavenCentral()
}
val atriumVersion by extra("0.18.0")
dependencies {
    testImplementation("ch.tutteli.atrium:atrium-fluent-en_GB:$atriumVersion")
}

We have defined a dependency to the bundle atrium-fluent-en_GB in the above example which provides a pure fluent API (in en_GB) for the JVM platform.

Have a look at the JVM sample projects for a quick setup, or Maven sample project if you prefer Maven to Gradle.

We currently provide the following extensions for the JVM platform:

  • kotlin_1_3: expectation functions for Kotlin 1.3 specific types (e.g. for Result).

You can enable them as follows:

dependencies {
    testImplementation("ch.tutteli.atrium:atrium-api-fluent-en_GB-kotlin_1_3:$atriumVersion")
}

Also take a look at Third-party Extensions which might come in handy as well.

click to see how the setup for the infix API looks like
repositories {
    mavenCentral()
}
val atriumVersion by extra("0.18.0")
dependencies {
    testImplementation("ch.tutteli.atrium:atrium-infix-en_GB:$atriumVersion")
}

And for the aforementioned extensions:

dependencies {
    testImplementation("ch.tutteli.atrium:atrium-api-infix-en_GB-kotlin_1_3:$atriumVersion")
}


maven:
Because maven is a bit more verbose than gradle, the example is not listed here but a sample maven project is provided which shows all necessary setup.

That is all, you are all set. Jump to Examples which shows how to use Atrium.

JS

build.gradle.kts:

repositories {
    mavenCentral()
}
val atriumVersion by extra("0.18.0")
dependencies {
    testImplementation("ch.tutteli.atrium:atrium-fluent-en_GB-js:$atriumVersion")
}

We have defined a dependency to the bundle atrium-fluent-en_GB-js in the above example which provides a pure fluent API (in en_GB) for the JS platform.

Have a look at the JS sample projects for a quick setup.

Otherwise, you need to setup an explicit dependency on atrium-fluent-en_GB-js in your test code in order that you can use Atrium. This is due to the loosely coupled design of Atrium and dead code elimination performed by the Kotlin compiler for JS.

Atrium itself is using mocha as well (see build.gradle -> createJsTestTask) and has tests written in JS modules (see AdjustStackTest) as well as tests written in common modules (e.g. SmokeTest) which are executed on the JS platform as well (actually on all platforms -> JVM uses JUnit for this purpose, see build.gradle -> useJupiter).

We currently provide the following extensions for the JS platform:

  • kotlin_1_3: expectation functions for Kotlin 1.3 specific types (e.g. for Result).

You can enable them as follows:

dependencies {
    testImplementation("ch.tutteli.atrium:atrium-api-fluent-en_GB-kotlin_1_3-js:$atriumVersion")
}
click to see how the setup for the infix API looks like
repositories {
    mavenCentral()
}
val atriumVersion by extra("0.18.0")
dependencies {
    testImplementation("ch.tutteli.atrium:atrium-infix-en_GB-js:$atriumVersion")
}

and for the aforementioned extensions:

dependencies {
    testImplementation("ch.tutteli.atrium:atrium-api-infix-en_GB-kotlin_1_3:$atriumVersion")
}

That is all, you are all set. Jump to Examples which shows how to use Atrium.

Android

Starting with 0.12.0 we no longer deliver a dedicated -android jar. Instead, you can use the same setup as shown in JVM setup. We start adding one again in case we have Android specific expectation functions.

Also take a look at Third-party Extensions which might come in handy as well.

Common

The setup for using Atrium in a common module of a multiplatform project is basically the same as for the JVM setup, you only need to suffix the dependency with -common in addition. For instance atrium-fluent-en_GB-common instead of atrium-fluent-en_GB.

Have a look at JVM, JS or Android to see how the setup of a specific platform has to be done. You might want to have a look at the Multiplatform sample project as well for a quick setup.

Examples

We are using the API provided by the bundle module atrium-fluent-en_GB 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

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 I expected the subject of the expectation, which was 10, to equal 9 -- and needlessly to say, this expectation was not met and thus the thrown error.

We are using the bundle atrium-fluent-en_GB 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

// two single expectations, only first evaluated
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:

// expectation-group with two expectations, both evaluated
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 { // if the previous block fails, then this one is not evaluated
    // ...
}

Expect an Exception

expect {
    // this lambda does something but eventually...
    throw IllegalArgumentException("name is empty")
}.toThrow<IllegalStateException>()

↑ Example ↓ Output

I expected subject: () -> kotlin.Nothing        (readme.examples.MostExamplesSpec$1$7$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.MostExamplesSpec$1$7$1.invoke(MostExamplesSpec.kt:67)
        ⚬ readme.examples.MostExamplesSpec$1$7$1.invoke(MostExamplesSpec.kt:22)
        ⚬ readme.examples.MostExamplesSpec$1$7.invoke(MostExamplesSpec.kt:280)
        ⚬ readme.examples.MostExamplesSpec$1$7.invoke(MostExamplesSpec.kt:22)

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.MostExamplesSpec$1$8$1 <1234789>)
◆ ▢ thrown exception when called: java.lang.IllegalArgumentException
    ◾ ▢ message: null
        ◾ to be an instance of type: 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.MostExamplesSpec$1$9$1 <1234789>)
◆ ▢ thrown exception when called: java.lang.IllegalArgumentException
    ◾ ▢ message: null
        ◾ to be an instance of type: 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 {
    // this block does something but eventually...
    throw IllegalArgumentException("name is empty", RuntimeException("a cause"))
}.notToThrow()

↑ Example ↓ Output

I expected subject: () -> kotlin.Nothing        (readme.examples.MostExamplesSpec$1$10$1 <1234789>)
◆ ▢ invoke(): ❗❗ threw java.lang.IllegalArgumentException
    β„Ή Properties of the unexpected IllegalArgumentException
      Β» message: "name is empty"        <1234789>
      Β» stacktrace: 
        ⚬ readme.examples.MostExamplesSpec$1$10$1.invoke(MostExamplesSpec.kt:88)
        ⚬ readme.examples.MostExamplesSpec$1$10$1.invoke(MostExamplesSpec.kt:22)
        ⚬ readme.examples.MostExamplesSpec$1$10.invoke(MostExamplesSpec.kt:89)
        ⚬ readme.examples.MostExamplesSpec$1$10.invoke(MostExamplesSpec.kt:22)
      Β» cause: java.lang.RuntimeException
          Β» message: "a cause"        <1234789>
          Β» stacktrace: 
            ⚬ readme.examples.MostExamplesSpec$1$10$1.invoke(MostExamplesSpec.kt:88)

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 (Spek, Kotest, and JUnit 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

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:

data class Person(val firstName: String, val lastName: String, val isStudent: Boolean) {
    fun fullName() = "$firstName $lastName"
    fun nickname(includeLastName: Boolean) = when (includeLastName) {
        false -> "Mr. $firstName"
        true -> "$firstName aka. $lastName"
    }
}

val myPerson = Person("Robert", "Stoll", false)

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) } // fails, subject still Person afterwards
    .its { fullName() }                   // not evaluated anymore, subject String afterwards
    .toStartWith("rob")                   // not evaluated anymore

↑ Example ↓ Output

I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false)        (readme.examples.FeatureExtractorSpec$1$Person <1234789>)
◆ ▢ its.definedIn(FeatureExtractorSpec.kt:43): 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:

  • the first expects 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.

    expect(myPerson) { // forms an expectation-group
    
        its({ firstName }) {   // forms an expectation-group
            toStartWith("Pe")  // fails
            toEndWith("er")    // is evaluated nonetheless
        }                      // fails as a whole
    
        // still evaluated, as it is in outer expectation-group
        its { lastName }.toEqual("Dummy")
    }

    ↑ Example ↓ Output

    I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false)        (readme.examples.FeatureExtractorSpec$1$Person <1234789>)
    ◆ ▢ its.definedIn(FeatureExtractorSpec.kt:52): "Robert"        <1234789>
        ◾ to start with: "Pe"        <1234789>
        ◾ to end with: "er"        <1234789>
    ◆ ▢ its.definedIn(FeatureExtractorSpec.kt:58): "Stoll"        <1234789>
        ◾ to equal: "Dummy"        <1234789>
    

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) } // fails, subject still Person afterwards
    .feature { f(it::fullName) }                     // not evaluated anymore, subject String afterwards
    .toStartWith("rob")                              // not evaluated anymore

↑ Example ↓ Output

I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false)        (readme.examples.FeatureExtractorSpec$1$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). it within the MetaFeature-provider-lambda refers to the subject of the expectation (myPerson 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) { // forms an expectation-group

    feature({ f(it::firstName) }) { // forms an expectation-group
        toStartWith("Pe")           // fails
        toEndWith("er")             // is evaluated nonetheless
    }                               // fails as a whole

    // still evaluated, as it is in outer expectation-group
    feature { f(it::lastName) }.toEqual("Dummy")
}

↑ Example ↓ Output

I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false)        (readme.examples.FeatureExtractorSpec$1$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 second for Pair and others. Please open a feature request 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.

πŸ’¬ Provide a feature extractor for each property?

You might be asking yourself whether it is better to write an own feature extractor or use feature.

The only drawback of using an existing property is that a few more keystrokes are required compared to writing an own feature extractor once and then reuse it (as we did with message). Yet, we do not recommend writing an own feature extractor for every single property. We think it makes sense to add one if you use it a lot and (preferably) it is a stable API. Why not always? Because one quickly forgets to rename the feature extractor if the property as such is renamed (e.g., as part of an IDE refactoring). As you can see, you would need to keep the property name and the name of the feature extractor in sync to be meaningful (otherwise one gets quickly confused or has to remember two names for the same thing).

Writing feature extractors for method calls is a different story though, especially due to overload bugs in Kotlin. Also, code completion is not yet as good as it should be when it comes to methods. Last but not least, in case it is not always safe to call a method (e.g. List.get => IndexOutOfBound) then it makes sense to wrap it into an own feature extractor and use _logic.extractFeature.

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) } // subject narrowed to String
    .toEqual("Robert aka. Stoll")       // fails
    .toStartWith("llotS")               // not evaluated anymore

↑ Example ↓ Output

I expected subject: Person(firstName=Robert, lastName=Stoll, isStudent=false)        (readme.examples.FeatureExtractorSpec$1$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) } // subject still Family afterwards
    .feature("the first member's name") { members.first().name }       // subject narrowed to String
    .toEqual("Peter")

↑ Example ↓ Output

I expected subject: Family(members=[FamilyMember(name=Robert)])        (readme.examples.FeatureExtractorSpec$1$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) }

fun <F : Any, T : Pair<F, *>> Expect<T>.firstToBe(expected: F) =
    feature(Pair<F, *>::first) { toEqual(expected) }

expect(listOf(1 to "a", 2 to "b")).get(10) {
    firstToBeDoneWrong(1)
    firstToBe(1)
}

↑ 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 Kotlin Bugs and upvote in case you run into one). It might happen that you run into such issues using feature in conjunction with a MetaFeature-provider-lambda (as shown in Property and Methods). However, Atrium provides alternative functions next to f within the MetaFeature-provider-lambda to disambiguate the situation. Use p for properties and f0 to f5 for methods. Likely you need to specify the type parameters manually as Kotlin is not able to infer them correctly.

class WorstCase {
    val propAndFun: Int = 1
    fun propAndFun(): Int = 1

    fun overloaded(): Int = 1
    fun overloaded(b: Boolean): Int = 1
}

expect(WorstCase()) {
    feature { p<Int>(it::propAndFun) }
    feature { f0<Int>(it::propAndFun) }
    feature { f0<Int>(it::overloaded) }
    feature { f1<Boolean, Int>(it::overloaded, true) }.toEqual(1)
}

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:

// java
class A { 
    public String getFoo() { return "bar"; } 
}
// kotlin
val a = A()
a.foo // syntactic sugar, accesses getFoo via property
expect(a)
    // feature{ f(it::foo) }    // would result in a compile error
    .feature { f(it::getFoo) }  // works
    .startsWith(...)

Type Expectations

interface SuperType

data class SubType1(val number: Int) : SuperType
data class SubType2(val word: String, val flag: Boolean) : SuperType

val x: SuperType = SubType2("hello", flag = true)
expect(x).toBeAnInstanceOf<SubType2> {
    feature { f(it::word) }.toEqual("goodbye")
    feature { f(it::flag) }.toEqual(false)
}

↑ 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

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. This is a shortcut for toBeAnInstanceOf<Xy> where Xy is the non-nullable type (see Type Expectations). Following an example:

expect(slogan2)        // subject has type String?
    .notToEqualNull()  // subject is narrowed to String
    .toStartWith("atrium")

↑ Example ↓ Output

I expected subject: null
◆ to be an instance of type: String (kotlin.String) -- Class: java.lang.String

Since notToEqualNull delegates to toBeAnInstanceOf, it also 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
◆ to be an instance of type: 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

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 toContain shortcut function which Atrium provides for Iterable<T> is kind of the opposite of inAnyOrder.atLeast(1) and is named toContainExactly. Again, Atrium provides two overloads for it, one for values, e.g. toContainExactly(1, 2) which calls toContain.inOrder.only.values(1, 2) and a second one which expects one or more assertionCreator-lambda, e.g. toContainExactly( { toBeGreaterThan(5) }, { toBeLessThan(10) }) which calls toContain.inOrder.only.elements({ toBeGreaterThan(5) }, { toBeLessThan(10) }). We will spare the examples here and show them in the following sections. Notice that you can pass null to toContainExactly instead of an assertionCreator-lambda to match null. This makes of course only sense if your Iterable 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: 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 toContain instead of one of the shortcut functions). Currently, the builder provides two options, either inAnyOrder or inOrder. 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 assertionCreator-lambda matched (signified by βœ” or ✘) the corresponding element or not (e.g. βœ˜β€„β–Άβ€„entry 1: was 2 and we expected, it to be less than 2)
  • Whether the expected size was correct or not (βœ˜β€„β–Άβ€„size: was 4, we expected it, to equal: 2
  • and last but not least, mismatches or additional elements as further clue (❗❗ additional elements detected).

😍 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 up to 10 elements, if the Iterable contains more elements, then only failing expectations are shown.

πŸ’¬ Show only failing expectations/elements earlier than 10 expected elements?

You can use the report option to specify when Atrium shall start to show only failing expectations. Following an example changing the limit to 3 elements by using showOnlyFailingIfMoreExpectedElementsThan :

expect(listOf(1, 2, 2, 4)).toContainExactly(
    { toBeLessThan(3) },
    { toBeLessThan(2) },
    { toBeGreaterThan(1) },
    report = { showOnlyFailingIfMoreExpectedElementsThan(2) }
)

↑ Example ↓ Output

I expected subject: [1, 2, 2, 4]        (java.util.Arrays.ArrayList <1234789>)
◆ ▢ size: 4        (kotlin.Int <1234789>)
    ◾ to equal: 3        (kotlin.Int <1234789>)
◆ to contain only, in order: 
  ⚬ ▢ element 1: 2        (kotlin.Int <1234789>)
      ◾ to be less than: 2        (kotlin.Int <1234789>)
    ❗❗ additional elements detected: 
       ⚬ element 3: 4        (kotlin.Int <1234789>)

Likewise, you can use showOnlyFailing() to set the limit to 0 and showAlwaysSummary() to set the limit to Int.MAX_VALUE


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

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 entries again provide two overloads, one expecting key-value Pairs:

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("bernstein") {
        feature { f(it::firstName) }.toEqual("Leonard")
        feature { f(it::age) }.toEqual(60)
    }
    .getExisting("einstein") {
        feature { f(it::firstName) }.toEqual("Albert")
    }

↑ 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.MostExamplesSpec$1$Person <1234789>)
    ◾ ▢ age: 50        (kotlin.Int <1234789>)
        ◾ to equal: 60        (kotlin.Int <1234789>)

In case you hvae 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-en_GB.

Path Expectations

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.resolve("atrium-path"))
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
πŸ’¬ Use because only to give reasons for non-obvious expectations

because can be a useful tool for explaining why there is a certain expectation. Sometimes it is not directly obvious why one should expect something. In such cases, using because can make your code, and your error messages, easier to understand for other developers (including yourself in three months).

Having said that, you should not use because if you are missing a specific predefined expectation function. You can use a feature extractor, write your own expectation function or propose an addition to Atrium in such cases.

Just like code comments, because can be valuable, but should not be overused.

Data Driven Testing

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.

The trick is to wrap your expectations into an expectation-group, use Feature Extractors and state expectations about those feautres. Following an example:

fun myFun(i: Int) = (i + 97).toChar()

expect("calling myFun with...") {
    mapOf(
        1 to 'a',
        2 to 'c',
        3 to 'e'
    ).forEach { (arg, result) ->
        feature { f(::myFun, arg) }.toEqual(result)
    }
}

↑ Example ↓ Output

I expected subject: "calling myFun with..."        <1234789>
◆ ▢ myFun(1): 'b'
    ◾ to equal: 'a'
◆ ▢ myFun(3): '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).

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:

import ch.tutteli.atrium.logic.utils.expectLambda

expect("calling myFun with ...") {
    mapOf(
        1 to expectLambda<Char> { toBeLessThan('f') },
        2 to expectLambda { toEqual('c') },
        3 to expectLambda { toBeGreaterThan('e') }
    ).forEach { (arg, assertionCreator) ->
        feature({ f(::myFun, arg) }, assertionCreator)
    }
}

↑ Example ↓ Output

I expected subject: "calling myFun with ..."        <1234789>
◆ ▢ myFun(3): 'd'
    ◾ to be greater than: 'e'

The example should be self-explanatory. One detail to note though is the usage of expectLambda. It is a helper function which circumvents certain Kotlin type inference bugs (upvote them please). Writing the same as mapOf<Int, Expect<Char>.() -> Unit>( 1 to { ... } ) would not work as the type for a lambda involved in a Pair is not (yet) inferred correctly by Kotlin.

There is one last 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):

fun myNullableFun(i: Int) = if (i > 0) i.toString() else null

expect("calling myNullableFun with ...") {
    mapOf(
        Int.MIN_VALUE to expectLambda<String> { toContain("min") },
        -1 to null,
        0 to null,
        1 to expectLambda { toEqual("1") },
        2 to expectLambda { toEndWith("2") },
        Int.MAX_VALUE to expectLambda { toEqual("max") }
    ).forEach { (arg, assertionCreatorOrNull) ->
        feature { f(::myNullableFun, arg) }.toEqualNullIfNullGivenElse(assertionCreatorOrNull)
    }
}

↑ Example ↓ Output

I expected subject: "calling myNullableFun with ..."        <1234789>
◆ ▢ myNullableFun(-2147483648): null
      Β» to contain: 
        ⚬ value: "min"        <1234789>
            Β» but no match was found
◆ ▢ myNullableFun(2147483647): "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. Have a look at apis/differences.md for a few more examples. This site contains also a list of all APIs with links to their expectation function catalogs.

You can also have a look at the specifications for more examples.

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 😊.

Third-party Extensions

Following extensions are maintained outside of this repository.

How is Atrium different from other Expectation/Assertion Libraries

The following subsections shall give you a quick overview how Atrium differ 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, so that you get the extra help needed.

πŸ’© <- this icon signifies a bug in Kotlin which you might encounter as well. We try to provide a workaround whenever possible.

πŸ’© There is no KDoc for toEqual

There is, but IntelliJ will not show it to you due to this bug (please upvote it). You should be able to see the KDoc of other functions without problems. But in case, you can also browse the online documentation, e.g. KDoc of toEqual.

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.MostExamplesSpec$1$40$1 <1234789>)
◆ ▢ thrown exception when called: java.lang.IllegalArgumentException
    ◾ to be an instance of type: IllegalStateException (java.lang.IllegalStateException)
      Β» ▢ message: 
          ◾ to be an instance of type: 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.MostExamplesSpec$1$40$1.invoke(MostExamplesSpec.kt:256)
        ⚬ readme.examples.MostExamplesSpec$1$40$1.invoke(MostExamplesSpec.kt:22)
        ⚬ readme.examples.MostExamplesSpec$1$40.invoke(MostExamplesSpec.kt:280)
        ⚬ readme.examples.MostExamplesSpec$1$40.invoke(MostExamplesSpec.kt:22)
      Β» cause: java.lang.UnsupportedOperationException
          Β» message: "not supported"        <1234789>
          Β» stacktrace: 
            ⚬ readme.examples.MostExamplesSpec$1$40$1.invoke(MostExamplesSpec.kt:254)

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 toEqual and of notToEqual for BigDecimal was introduced which are both deprecated and throw a PleaseUseReplacementException. The reason behind it? It is very likely that a user actually wants to compare that a certain BigDecimal is numerically (not) equal to another BigDecimal rather than including BigDecimal.scale in the comparison. Accordingly, the deprecation message of toEqual (notToEqual alike) explains the problem and suggests to either use toEqualNumerically or toEqualIncludingScale. And if the user should decide to use toEqualIncludingScale and at some point an expectation fails only due to the comparison of BigDecimal.scale 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 expectationCreator-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-en_GB. You prefer pure fluent and do not even want to see infix style in your code, then use atrium-fluent-en_GB 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 1.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.

Internationalization

The last difference is not yet fully-blown implemented, but the design of Atrium has everything needed to go down the planed Roadmap. Might well be that this topic is not really a concern of yours; unless...

  • you are using domain-driven-design and would like to adopt the ubiquitous language also to your test code.
  • you want to document the results of your defined expectations (in different languages)

Atrium already supports APIs in two languages, and it is an easy task to translate an API to another language (hello DDD-people πŸ‘‹ you are good to go). Moreover, it is already possible to generate the output in a different language than the used API (e.g. code in English but report in German).

Together with the HTML-Report feature (currently missing but will follow) you will be able to generate reports in different languages. Already the HTML-Report feature as such might be of your interest. You can use it to document your user stories etc (almost) for free. In case you have clients who speak different languages then the HTML-Report together with the i18n feature will be especially helpful. We should not go on here, the HTML-Report feature is not yet implemented, but you can see what kind of road we plan to go down to.

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.

  1. Choose the extension point: in our example we want to provide the expectation function for Ints. Hence, we define toBeAMultipleOf as extension function of Expect<Int>.

  2. Use the method _logic.createAndAppend which creates and appends the expectation to itself (creating alone is not enough, it needs to be appended in order that it is evaluated). The method createAndAppend returns an Expect for the current subject, making it easy for you to provide a fluent API as well.

    The method createAndAppend expects:

    • either a String or a Translatable as description of your expectation.
    • the representation of the expected value.
    • and the actual check as lambda where you typically use it which refers to the subject of the expectation.

We used a String as description in the above example because we are not bothered with internationalization at this point (have a look at Internationalization if you do).

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

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) =
    toBeGreaterThanOrEqualTo(lowerBoundInclusive).and.toBeLessThan(upperBoundExclusive)

Pretty simple, isn't it? Notice though, that this function fails fast, which means, the upper bound is not evaluated if the expectation about the lower bound already fails. You need to use an expectation-group if you want that both are evaluated:

import ch.tutteli.atrium.logic._logic

fun <T : Date> Expect<T>.toBeBetween(lowerBoundInclusive: T, upperBoundExclusive: T) =
    _logic.appendAsGroup {
        toBeGreaterThanOrEqualTo(lowerBoundInclusive)
        toBeLessThan(upperBoundExclusive)
    }

Still simple enough.

πŸ’¬ Why is a type parameter used in the above examples?

That is right, we used a type parameter T: Date and not Expect<Date> directly. You should always do this unless your type is final (not open) and does not have type parameters itself - but to have a simple rule, just do it. This way the expectation function is also available for sub types. This is because Expect is invariant. Following an example:

interface A { val foo get() = 1 }
class B: A
val Expect<A>.foo get() = feature(A::foo)

expect(B()).foo // does not compile as foo is only available for `Expect<A>`

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>
    // ...  and others
)

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:

  1. we make use of a feature extractor with class reference.
  2. We use the overload which expects an assertionCreator-lambda. This way subsequent expectations are still made on Person and not on children.

Its usage is then as follows:

expect(Person("Susanne", "Whitley", 43, listOf()))
    .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, listOf()))
    .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, listOf()))))
    .children { // using the fun -> expectation-group, ergo sub expectations don't fail fast
        toHaveElementsAndNone {
            feature { f(it::firstName) }.toStartWith("Ro")
        }
        toHaveElementsAndAll {
            feature { f(it::lastName) }.toEqual("Whitley")
        }
    } // subject is still Person here
    .children  // using the val -> subsequent expectations are about children and fail fast
        .toHaveSize(2)
        .toHaveElementsAndAny {
            feature { f(it::age) }.toBeGreaterThan(18)
        }

↑ 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 Pairs. We want to assert that the pairs contain only the first name / last name pairs of certain Persons in any order. Collection Expectations will help us with this. However, toContain.inAnyOrder.values expects Pairs. So we have to map from Person to Pair 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 map from Kotlin. And it gets worse if we want to use toContain.inAnyOrder.entries which expects at least one assertionCreator-lambda (Expect<T>.() -> Unit) 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 give 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:

  • all expectation functions on the logic level (what you have seen in Compose expectation functions was the API level) so that you can reuse and compose them in other ways.
  • changeSubject which allows to change the subject either:
    • unreported; meaning it does not show up in reporting (e.g. Expect<Array<out T>>.asList() uses it, see arrayAssertions)
    • reported, using reportBuilder; meaning a subject transformation which is shown in reporting as it incorporates a transformation (e.g. toBeAnInstanceOf uses it, see AnyAssertions)
  • collect which allows to collect expectations - especially helpful in composing expectations (see mapAssertions)
  • extractFeature for feature extraction where it is not always save to extract (see List.get)

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).

We are willing to provide more documentation if you need it (please open an issue). In the meantime we might help you via slack, please post your questions in the atrium Slack channel (Invite yourself in case you do not have an account yet).

Use own Expectation Verb

Atrium offers the expectation verb expect out of the box.

You can also define your own expectation verb if expect does not suite you or in case you want to change some default implementation. In order to create an own expectation verb it is sufficient to:

  1. Copy the file content of atriumVerbs.kt
  2. Create your own atriumVerbs.kt and paste the previously copied content -- notice that you can also use a String for the expectation verb in case you do not care about Internationalization
  3. Adjust package name and imports and rename expect as desired (you can also leave it that way of course).
  4. exclude atrium-verbs from your dependencies. Taking the setup shown in the Installation section for the JVM platform, you would replace the dependencies block as follows:
    dependencies {
        testImplementation("ch.tutteli.atrium:atrium-fluent-en_GB:$atriumVersion") {
            exclude group: 'ch.tutteli.atrium', module: 'atrium-verbs'
        }
    }

What are the benefits of creating an own expectation verb:

  • you can encapsulate the reporting style.
    This is especially useful if you have multiple projects and want to have a consistent reporting style.
    For instance, you could change from same-line to multi-line reporting or report not only failing but also successful expectations, change the output language etc.

    πŸ’¬ where should I put the atriumVerbs.kt?

    We suggest you create an adapter project for Atrium where you specify the expectation verb. And most likely you will accumulate them with expectation functions which are so common that they appear in multiple projects -- please share them with us (get in touch with us via issue or slack) if they are not of an internal nature πŸ˜‰


What are the drawbacks:

  • you have to maintain your expectation verb. That should not be a big deal though -- you might have to replace deprecated options by their replacement when you upgrade to a newer Atrium version but that's about it.

Use own Components

Replacing existing components with your own (or third-party) components can be done when specifying an own expectation verb via withOptions. See for instance atriumVerbs.kt which is used internally of Atrium in tests and uses a different AtriumErrorAdjuster.

Another example, say you prefer multi-line reporting over single-line reporting, then you can use withOptions as follows:

import ch.tutteli.atrium.core.ExperimentalNewExpectTypes
import ch.tutteli.atrium.creating.ExperimentalComponentFactoryContainer
import ch.tutteli.atrium.creating.build

@OptIn(ExperimentalNewExpectTypes::class, ExperimentalComponentFactoryContainer::class)
fun <T> expect(subject: T): RootExpect<T> =
    RootExpectBuilder.forSubject(subject)
        .withVerb("expected the subject")
        .withOptions {
            withComponent(TextAssertionPairFormatter::class) { c ->
                TextAssertionPairFormatter.newNextLine(c.build(), c.build())
            }
        }
        .build()

Following an example using the expectation verb

expect(10).toEqual(9)

↑ Example ↓ Output

expected the subject:
  10        (kotlin.Int <1234789>)
◆ to equal:
  9        (kotlin.Int <1234789>)

Compare the above output with what we would get per default:

expected the subject: 10        (kotlin.Int <1234789>)
◆ to be: 9        (kotlin.Int <1234789>)

You prefer another reporting style but Atrium does not yet support it? Please let us know it by writing a feature request.

There are more options to choose from. Take a look at the DefaultComponentFactoryContainer to see the default configuration.

Internationalization

We distinguish between two use cases. You might want to generate the Report in a different language or/and you might want to use the API in a different language.

Report

Following on the example in Write own Expectation Functions we show here how you need to write the toBeAMultipleOf function, so that it supports i18n. This way the report could be generated in another language.

The difference lies in the first argument passed to createAndappend; we do no longer use a String but a proper Translatable.

import ch.tutteli.atrium.logic.*

fun Expect<Int>.toBeAMultipleOf(base: Int): Expect<Int> =
    _logic.createAndAppend(DescriptionIntAssertion.TO_BE_A_MULTIPLE_OF, base) { it % base == 0 }

enum class DescriptionIntAssertion(override val value: String) : StringBasedTranslatable {
    TO_BE_A_MULTIPLE_OF("to be a multiple of")
}

Typically, you would put DescriptionIntAssertion into an own module (jar) so that it could be replaced (with zero performance cost) by another language representation. For instance, atrium-fluent-en_GB-common uses atrium-translations-en_GB whereas tests of atrium-infix_en_GB-common uses atrium-translations-de_CH.

πŸ’¬ Using a TranslationSupplier

Next to providing translations via code you can also use a TranslationSupplier based Translator by configuring the ReporterBuilder accordingly (e.g. use withDefaultTranslationSupplier instead of withoutTranslations). Atrium supports a properties files based TranslationSupplier for JVM (a supplier for JS will follow) which is more or less what ResourceBundle provides out of the box. Yet, a Translator uses a more enhanced fallback mechanism compared to a ResourceBundle. For further technical information have a look at the KDoc of Translator. Notice though, that we plan to move away from the ResourceBundle-inspired approach due to encoding problems and missing implementations on other platforms than JVM.

Notice, Atrium does not yet support the generation of multiple reports in the same test run. This might become handy if you want to generate an HTML report in different languages.
However, Atrium is designed to support this use case -- if you need this feature, then please let us know it by writing a feature request.



Let us rewrite the toBeEven expectation function from the section Write own Expectation Functions as second example:

import ch.tutteli.atrium.logic.*

fun Expect<Int>.toBeEven(): Expect<Int> =
    _logic.createAndAppend(DescriptionBasic.TO_BE, DescriptionIntAssertions.EVEN) { it % 2 == 0 }

enum class DescriptionIntAssertions(override val value: String) : StringBasedTranslatable {
    EVEN("an even number")
}

Once again we have to wrap the text which we want to be able to exchange with another language into a Translatable. Notice also, that we are reusing a Translatable from DescriptionBasic.

API in a different Language

Following on the example in the previous section, we want to write toBeAMultipleOf in such a way that one cannot only generate a report in a different language but also that one can use the function itself in a different language. Or in other words, provide our API in a different language (the same applies if you want to provide another API style).

We split up the function in two parts: API and logic -- whereas the logic creates the expectation and the API provides a function for the user (the API as such) and merely appends the expectation created by the logic to the Expect.

Typically, you put the API function in one module (jar) and the logic in another (so that the API can be exchanged). In the logic module we define an extension method for AssertionContainer

import ch.tutteli.atrium.creating.AssertionContainer

fun AssertionContainer<Int>.toBeAMultipleOf(base: Int): Assertion =
    createDescriptiveAssertion(DescriptionIntAssertion.TO_BE_A_MULTIPLE_OF, base) { it % base == 0 }

In the above example we created a simple DescriptiveAssertion with the help of createDescriptiveAssertion defined on AssertionContainer. We pass in a description (TO_BE_A_MULTIPLE_OF), we use base as representation of the expectation and define a lambda which implements a test to define whether the expectation holds or not.

In the API module we define the extension function and append the expectation to the current Expect by using logicAppend and calling the extension function from the logic module within the corresponding lambda.

import ch.tutteli.atrium.logic.*

fun Expect<Int>.toBeAMultipleOf(base: Int): Expect<Int> =
    _logicAppend { toBeAMultipleOf(base) }

At first, this looks like a recursive call. But as explained, within the _logicAppend-lambda we are on the logic level and thus call the function we defined above (which just turns out to have the same name).

You are ready to go, creating an API in a different language -- e.g. in German -- is now only a routine piece of work:

import ch.tutteli.atrium.logic.*

fun Expect<Int>.istEinVielfachesVon(base: Int): Expect<Int> =
    _logicAppend { toBeAMultipleOf(base) }

API Styles

Atrium supports currently two API styles: pure fluent and infix. Both have their design focus on interoperability with code completion functionality of your IDE -- so that you can let your IDE do some of the work.

Atrium is built up by different modules and it is your choice which implementation you want to use. However, this is more intended for advanced user with special requirements. Atrium provides bundle modules which bundle API, logic, core, translation as well as predefined expectation verbs, so that you just have to have a dependency on one of those bundles (kind a bit like a BOM pom in the maven world):

Have a look at apis/differences.md for more information and to see how the API styles differ.

Java Interoperability

Atrium provides some helper functions in case you have to deal with Java Code where not all types are non-nullable. Platform types are turned into a non-nullable version per default (if possible).

Yet, that might not be what you want, especially if you know that certain functions return potentially null or in other words, the return type of those functions should be treated as nullable in Kotlin. Therefore, you want to turn the platform type into the nullable version.

You need to use a cast to do this. But depending on your return type this might be cumbersome especially if you deal with type parameters. Thus, Atrium provides the following functions to ease dealing with Java Code at least for some standard cases:

  • nullable turns a type into a nullable type and a return type of a KFunction into a nullable type.
  • nullableContainer turns an Iterable into an iterable with nullable element type, likewise it does the same for Array.
  • nullableKeyMap turns a Map into a map with a nullable key type.
  • nullableValueMap turns a Map into a map with a nullable value type.
  • nullableKeyValueMap turns a Map into a map with a nullable key and nullable value type.

KDoc - Code Documentation

The code documentation is generated with dokka and is hosted on github-pages: KDoc of atrium

Known Limitations

According to the YAGNI principle this library does not yet offer a lot of out-of-the-box expectation 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.

FAQ

You find frequently asked questions below. If your question is not answered below, then please do not hesitate and ask your question in the atrium Slack channel. In case you do not have an account for kotlinlang.slack.com yet, then please Invite yourself.

Are there toContain/toHaveNextAndAll/None/All expectation functions for Sequence/Array?

Atrium does not provide extension function applicable to Expect<Sequence<E>> (or Array) directly, because they would basically duplicate the functions available for Iterable<E>.
However, Atrium provides asIterable and asList so that you can turn Expect<Sequence<E>> into Expect<Iterable<E>> or Expect<List<E>>. An example:

expect(sequenceOf(1, 2, 3)).asIterable().toContain(2)

Likewise, you can turn an Expect<Array<E>>, Expect<DoubleArray> etc. into an Expect<List<E>> with asList.

Feel free vote for first class support for Array and Sequence in api-fluent.

πŸ’¬ why do I not see anything about the transformation in reporting?

asIterable uses _logic.changeSubject.unreported internally which is intended for not showing up in reporting. If you would like that the transformation is reflected in reporting then you can use a regular feature extractor as follows:

expect(sequenceOf(1, 2, 3)).feature { f(it::asIterable) }.toContain(2)

Where do I find a list of all available functions?

Atrium provides KDoc for all APIs - have a look at their KDoc:

Problems in conjunction with feature

See Ambiguity Problems and Property does not exist.

Roadmap

The roadmap is maintained at atrium-roadmap. The milestones give you an overview of the planned (breaking) changes -- e.g. the changes for the next major version 1.0.0

You are invited to take part in the discussions related to design decisions, upcoming features and more. Bring in your own wishes and ideas into this process.

In case you are missing a particular expectation function in Atrium, then please open a Feature Request in this repository.

Contributors and contribute

Our thanks go to code contributors as well as other contributors (see acknowledgements in the release notes).

You are more than welcome to contribute as well:

  • star Atrium if you like it
  • open a bug or create a feature request
  • share your ideas via issue or slack
  • ask a question so that we better understand where Atrium needs to improve.
  • write a blog post about Atrium (e.g. about a feature you like) or a tutorial (let us know we happily link to your page)
  • share your expectation functions with the rest of us by creating a pull request (no need for i18n support or the like, we can augment your pull request).
  • have a look at the help wanted issues if you would like to code (ping us on Slack if there are not any).

Please have a look at CONTRIBUTING.md for further suggestions and guidelines.

Sponsors

We would like to thank the following sponsors for their support:

Do you want to become a sponsor as well? Great, have a look at the following GitHub sponsor profiles:

  • robstoll (Author and main contributor)

or ping @robstoll in the Slack-Channel if you would like to support the project in another way.

License

Atrium is licensed under EUPL 1.2.

Atrium is using:

About

A multiplatform assertion library for Kotlin

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Kotlin 99.4%
  • Other 0.6%