Baeldung Pro – Kotlin – NPI EA (cat = Baeldung on Kotlin)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Introduction

Kluent is a fluent test assertion library for Kotlin. It allows us to write assertions in a highly fluent style while still being typesafe and extensible.

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

2. Dependencies

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

If we’re using Maven, we can include this dependency in our pom.xml file:

<dependency>
    <groupId>org.amshove.kluent</groupId>
    <artifactId>kluent</artifactId>
    <version>1.73</version>
    <scope>test</scope>
</dependency>

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

testImplementation("org.amshove.kluent:kluent:1.73")

This is specifically a test scope dependency rather than the standard compile scope since we want only our test code to be able to use Kluent.

If we’re targeting Android, we’ll need to include org.amshove.kluent:kluent-android instead.

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

3. Simple Assertions

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

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

import org.amshove.kluent.*

Here, we’re using a wildcard import for simplicity, but we could also import each assertion method instead.

Once we’ve done this, we’ve got access to our assertion functions. These assertions are all available as extension functions, allowing us to write much more readable code:

value.shouldBeEqualTo("Expected")

They’re also all written as infix functions. As such, we can omit the period and brackets to make the calls read like regular sentences:

value shouldBeEqualTo "Expected"

There are also backtick versions of most of our assertions to make the code read even more like a regular sentence:

value `should be equal to` "Expected"

All three of these examples are the same and equivalent to the assertEquals() method from JUnit. The output even looks similar if the assertion fails:

org.amshove.kluent.internal.ComparisonFailedException: Expected: <Expected> but was: <Hello>
Expected :<Expected>
Actual   :<Hello>

Most of the assertions have negative versions, too. For example, we can use shouldNotBeEqualTo() to assert that two values are different from each other:

value shouldNotBeEqualTo "Expected"

4. Type-Safe Assertions

Because Kluent uses extension functions to provide fluent assertions, the assertions are more type-safe than libraries such as JUnit.

For example, if our actual value is a string, then we can do case-insensitive assertions:

value shouldBeEqualToIgnoringCase "Expected"

There are versions of this assertion function defined as an extension function on the kotlin.Char, kotlin.CharSequence, and java.lang.String classes. This means that this assertion is only available on variables of these types. Therefore, if our value is any other type, this method isn’t available, and so, this becomes a compilation error.

This works with the assertion argument, too, in that the parameters must be the correct type. For example, doing a shouldBeGreaterThan assertion on a Long requires that the provided type is Comparable<Long>. As before, if not, then this won’t compile:

1L shouldBeGreaterThan Short.MIN_VALUE

Instead of comparing against a Short, we must compare against another Long:

1L shouldBeGreaterThan Long.MIN_VALUE

Depending on the extension function signature, this type-safety isn’t always guaranteed. For example, the shouldBeEqualTo assertion is defined on an unbounded generic type, meaning that this will automatically collapse down to the best common type – which may be Any.

This means the compiler allows us to use this assertion on any arbitrary pair of types. However, this is correct behavior since we can also define our equals() methods to compare arbitrary types.

5. Equivalency Assertions

The simple way of asserting that two values are equal is to use the shouldBeEqualTo() method. However, this asserts equality by calling the equals() method on the objects – which means that it can only determine equality based on what this method does.

For example, given the following class:

class NotAllFieldsChecked(val id: String, val name: String, val age: Int) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as NotAllFieldsChecked

        return id == other.id
    }

    override fun hashCode(): Int {
        return id.hashCode()
    }
}

The shouldBeEqualTo() assertion will succeed if the two instances have the same id value, regardless of the other fields.

In some cases, this isn’t what we want. Kluent offers an alternative whereby we can assert that two values are equivalent to each other. This asserts that every field matches between the two objects:

@OptIn(ExperimentalStdlibApi::class)
@Test
fun assertEquivalent() {
    val actual = NotAllFieldsChecked("123", "Baeldung", 42)

    actual.shouldBeEquivalentTo(NotAllFieldsChecked("123", "Other", 100))
}

The shouldBeEquivalentTo assertion uses reflection to check all fields of the two objects being compared. This test will fail because the fields are different, even though the equals() method would return true in this case.

Note that this API isn’t marked as an infix API, so we use it as a traditional function expression. Kluent has also marked it as being experimental, which means that we need to annotate our test method with @OptIn to use it.

We can also control how the matching works by providing a lambda block after the assertion call, in which we can configure the equivalency checks:

actual.shouldBeEquivalentTo(NotAllFieldsChecked("123", "Other", 42)) {
    it.excluding(NotAllFieldsChecked::name)
}

This is especially useful if we want to control the set of fields that the assertion checks.

6. Asserting Exceptions

As well as standard assertions on values, we can also assert that a particular piece of code should or shouldn’t throw an exception. We achieve this with the assertions shouldThrow() and shouldNotThrow().

Because we’re asserting on something throwing, we need to indicate that we want Kluent to run the appropriate block of code. We do this with the invoking() call. We can then chain our assertion onto this as appropriate:

invoking { 1 / 0 } shouldThrow ArithmeticException::class

We’re also able to further tailor our assertion by checking the cause or the message of the exception that was thrown:

invoking { 1 / 0 } shouldThrow ArithmeticException::class withMessage "/ by zero"

This will then fail the assertion if the exception wasn’t thrown, or if the message wasn’t what we expected.

Kluent also supports suspend functions for these assertions by using the coInvoking() method instead:

coInvoking { suspend { throw TimeoutException() }() } shouldThrow TimeoutException::class

This will then correctly invoke our suspend function and correctly assert any exceptions thrown from it.

7. Multiple Assertions

Every assertion we’ve seen so far will immediately fail the test. This is common behavior, but it can be frustrating when we have several related assertions and lose information by only getting the first error when multiple failing assertions provide more clarity.

Kluent supports soft assertions, whereby a group of assertions can be wrapped together so that they’ll all run, and then the test fails afterward if any of these assertions fail. Using assertSotfly() informs us about all of the failures in the group, instead of just the first one:

val set = setOf("a", "b", "c")

assertSoftly {
    set shouldHaveSize 4
    set shouldContain "a"
    set shouldContain "b"
    set shouldContain "c"
    set shouldContain "d"
}

In this case, we’re expecting our set to have exactly four items, and for those items to be the ones listed. When more than one of these assertions fails, then the use of assertSoftly() means we’ll see all the failures at once:

org.amshove.kluent.MultiAssertionError: The following 2 assertions failed:
1) Expected collection size to be 4 but was 3
at com.baeldung.kluent.SimpleAssertionsUnitTest.softAssertions(SimpleAssertionsUnitTest.kt:30)
2) Iterable doesn't contain "d"
Expected: the Iterable to contain "d"
Actual: a, b, c
at com.baeldung.kluent.SimpleAssertionsUnitTest.softAssertions(SimpleAssertionsUnitTest.kt:34)

Here, we can tell that the set contained three items instead of four and that the element “d” was missing. Without the assertSoftly() wrapper, we’d have only found out that the set was the wrong size, and not what the missing element was.

8. Summary

This was a quick introduction to Kluent. The Kluent library offers rich Kotlin support via its use of extension and infix functions and handles coroutines and suspend functions natively. We’ve only covered the basics of this library, but it is capable of handling many advanced testing scenarios.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.