Rich Assertions
In Part 1, we saw how you can write assertions
in Kotest using matchers, and we considered a single example of a
matcher: shouldBe.
You can do a lot with shouldBe, but Kotest provides a large set of matchers
that perform other kinds of assertion. Using these can simplify unit tests
and make them much easier to read.
We present examples of some of these below, but for full details you should consult the Kotest documentation for Core Matchers and Collections Matchers.
Floating-Point Values
When testing the result of a floating-point expression, shouldBe won’t
suffice on its own, due to the inexactness of floating-point arithmetic.
Instead, we need to test whether the result of the expression is close
enough to the expected value.
We achieve this by combining shouldBe with another matcher, plusOrMinus.
This allows us to specify how much deviation above or below the expected
value is considered acceptable.
For example, to test whether variable result is close enough to 3.0,
we could use
result shouldBe (3.0 plusOrMinus 1e-6)
The parentheses are needed here to ensure that the plusOrMinus matcher
takes priority over shouldBe.
This assertion would succeed if the value of result was between 2.999999
and 3.000001.
plusOrMinus is in the package io.kotest.matchers.doubles. You will need
an import statement like this in order to use it:
import io.kotest.matchers.doubles.plusOrMinus
Comparisons
Consider how we might assert that the result of some computation is a value
less than 10. We could do this using shouldBe like so:
result < 10 shouldBe true
Alternatively, we could write
result shouldBeLessThan 10
This is clearer and more readable.
Kotest also provides the other comparison matchers that you would expect:
shouldBeGreaterThan
shouldBeLessThanOrEqualTo
shouldBeGreaterThanOrEqualTo
These can be used to test values of any type for which the standard comparison operators are defined.
These matchers are provided in the package io.kotest.matchers.comparables.
You will need the appropriate import statement to use one of them, e.g.,
import io.kotest.matchers.comparables.shouldBeLessThan
Testing Strings
Consider how we might test whether the string resulting from some text manipulation was blank, or was empty, or started with a particular sequence of characters. We could make the necessary assertions like this:
str.isEmpty() shouldBe true
str.isBlank() shouldBe true
str.startsWith("Foo") shouldBe true
Alternatively, we could use Kotest’s string matchers:
str.shouldBeEmpty()
str.shouldBeBlank()
str shouldStartWith "Foo"
Again, the benefit here is improved readability.
Notice the change in syntax here. Asserting that a string should be empty, for example, doesn’t require an additional argument, therefore we cannot use infix notation for the call.
Some of the string matchers are particularly powerful. For example, if you need to assert that a string should contain at least one decimal digit, or that it contains only decimal digits, or that it is valid representation of an integer in a particular number base, you can use
str.shouldContainADigit()
str.shouldContainOnlyDigits()
str.shouldBeInteger() // decimal representations
str.shouldBeInteger(radix=16) // hexadecimal representations
str.shouldBeInteger(radix=2) // binary representations
There are many other string matchers. See the documentation for details of these.
These matchers are provided in the package io.kotest.matchers.string.
You will need the appropriate import statement to use one of them, e.g.,
import io.kotest.matchers.string.shouldBeEmpty
Testing Collections
If the result of computation is a collection of some kind, then we could
make assertions about emptiness, size and contents using shouldBe, like so:
result.isEmpty() shouldBe true
result.size shouldBe 10
42 in result shouldBe true
Alternatively, we could use others matchers to make these assertions in a clearer way:
result.shouldBeEmpty()
result shouldHaveSize 10
result shouldContain 42
We can also make more sophisticated assertions about collection contents:
result.shouldContainAll("a", "b", "c")
result.shouldContainExactly("a", "b", "c")
tesult.shouldContainExactlyInAnyOrder("a", "b", "c")
result.shouldBeSorted()
The first of these examples would succeed for any collection containing these
three strings and would allow other strings to be present. For example, it
would succeed for ["a", "b", "c"], ["b", "a", "c"] and
["a", "b", "c", "d"], but fail for ["a", "b", "d"].
The second example would succeed only for collections containing exactly
["a", "b", "c"]. It would fail for ["a", "b", "c", "d"] or
["b", "a", "c"].
The third example relaxes the ordering requirement, so would succeed for
both ["a", "b", "c"] and ["b", "a", "c"].
The final example is used specifically with lists, and would succeed for any list whose elements were sorted into their natural ascending order.
There are many other matchers that can be used with collections. See the documentation for details of these.
These matchers are provided in the package io.kotest.matchers.collections.
You will need the appropriate import statement to use one of them, e.g.,
import io.kotest.matchers.collections.shouldHaveSize
Inspectors
Kotest inspectors allow for more detailed assertions to be made about the
contents of a collection. For example, suppose you need to verify that
a particular collection of strings contains at least two strings that are
at least 10 characters long. You can do this using the forAtLeast()
inspector, with a lambda expression containing the shouldHaveMinLength
matcher:
result.forAtLeast(2) {
it shouldHaveMinLength 10
}
See the Inspectors documentation for more details.
Logical Negation
The matchers discussed above have involved ‘positive’ assertions that something is true about the value being tested, but it important to note that Kotest also provides matchers representing the logical negation of these assertions.
For example, you can use shouldNotBe to assert that the result of
computation should not have a particular value.
Other examples include
shouldNotBeEmptyandshouldNotBeBlank, for stringsshouldNotBeLessThan,shouldNotBeGreaterThan, etc, for comparisonsshouldNotHaveSizeandshouldNotContain, for collections
Testing For Exceptions
If your code throws exceptions, it’s essential to have some of your unit tests check whether these exceptions are thrown when expected.
Kotest provides the following functions for testing exceptions. Each of them accepts a lambda expression as an argument, containing the code that is supposed to trigger the exception. The first two are generic functions that also require an exception type to be specified.
shouldThrow
shouldThrowExactly
shouldThrowAny
For example, suppose you have a class Money, representing an amount of
money as euros and cents. Attempting to create a Money object with a
negative number of euros should trigger an IllegalArgumentException.
You could test for this behaviour using shouldThrow() like so:
"Exception when creating Money with negative euros" {
shouldThrow<IllegalArgumentException> {
Money(-1, 0)
}
}
This test will pass if creating a Money object with a negative value for
euros throws an instance of IllegalArgumentException or any of its
subclasses. If you want to exclude subclasses, use shouldThrowExactly
instead.
If you don’t care about the exception type and simply want to verify that an exception occurs, you can use simpler code to make that assertion:
shouldThrowAny { Money(-1, 0) }
Note that shouldThrow() and shouldThrowAny() return the exception object
that was thrown. You are free to ignore this, or you can use it to test the
error message associated with the exception, using the shouldHaveMessage
matcher:
"Exception when creating Money with negative euros" {
val exception = shouldThrow<IllegalArgumentException> {
Money(-1, 0)
}
exception shouldHaveMessage "Invalid euros"
}