Custom Assertions
One of the aims of Strikt is that implementing your own assertions is really, really easy.
Assertion functions are extension functions on the interface Assertion.Builder<T>
where T
is the type of the assertion subject.
Tip: Avoiding clashing method signatures
Because of Java's generic type erasure, it's possible that the name of your custom assertion function may clash with an existing one for a different subject type.
To avoid this you can add the @JvmName
annotation to your assertion function to disambiguate the methods.
See Handling signature clashes with @JvmName in the Kotlin documentation.
For example, if you were defining an isEmpty
assertion for Gson's JsonArray
type, you would need to disambiguate it from the isEmpty
assertion Strikt provides for collections.
@JvmName("isEmpty_JsonArray")
fun Assertion.Builder<JsonArray>.isEmpty(): Assertion.Builder<JsonArray> =
// ...
Atomic assertions
"Atomic" assertions produce a single message on failure.
They call assert
passing a lambda with the assertion logic that calls pass()
or fail()
.
The standard assertions isNull
, isEqualTo
, isA<T>
and many others are simple assertions implemented just like this.
Let's imagine we're implementing an assertion function for java.time.LocalDate
that tests if the represented date is a leap day.
fun Assertion.Builder<LocalDate>.isStTibsDay(): Assertion.Builder<LocalDate> =
assert("is St. Tib's Day") {
when (MonthDay.from(it)) {
MonthDay.of(2, 29) -> pass()
else -> fail()
}
}
Breaking this down:
- We declare the assertion function applies only to
Assertion.Builder<LocalDate>
. - Note that the function also returns
Assertion.Builder<LocalDate>
so we can include this assertion as part of a chain. - We call
assert
passing a description of the assertion and a lambda with the assertion logic. - If
it
(the test subject) is the value we want we callpass()
otherwise we callfail()
If this assertion fails it will produce a message like:
▼ Expect that 2018-05-01:
✗ is St. Tib's Day
Note
The method assert
accepts a description for the assertion being made and a lambda function Assertion<T>.(T) -> Unit
.
The parameter passed to the lambda is the assertion subject.
The Assertion<T>
receiver provides the lambda the pass()
and fail()
methods for reporting the assertion result.
Describing the "actual" value
For assertions that perform a comparison between actual and expected values it is usually helpful to describe the value that was incorrect. That way any assertion failure message will be more helpful.
In order to do this, Strikt provides an overridden version of fail()
that accepts a message string and the actual value found.
The message string should contain a format placeholder for the value.
fun Assertion.Builder<LocalDate>.isStTibsDay(): Assertion.Builder<LocalDate> =
assert("is St. Tib's Day") {
when (MonthDay.from(it)) {
MonthDay.of(2, 29) -> pass()
else -> fail(
description = "in fact it is %s",
actual = it
)
}
}
Now if the assertion fails there is a little more detail.
▼ Expect that 2018-05-01:
✗ is St. Tib's Day
in fact it is 2018-05-01
In this case that's not terribly helpful but when dealing with properties, method return values, or the like it can save a lot of effort in identifying the precise cause of an error. When comparing large string values it also means IDEs such as IntelliJ IDEA will present a detailed diff dialog in the event of a test failure.
Simple atomic assertions with boolean expressions
For the simplest assertion functions, instead of using assert
and calling pass
or fail
, you can use assertThat
with a lambda whose parameter is the assertion subject that returns a boolean.
We can re-implement the example above like this:
fun Assertion.Builder<LocalDate>.isStTibsDay(): Assertion.Builder<LocalDate> =
assertThat("is St. Tib's Day") {
MonthDay.from(it) == MonthDay.of(2, 29)
}
You should not use this form when you want to provide a meaningful description of the actual value but for simple assertions it's slightly less verbose.
Composed assertions
For more complex assertion implementations you can "nest" sub-assertions inside your overall assertion. Composed assertions' results are reported under the overall result which is useful for providing detailed diagnostic information in case of a failure.
Composed assertions are useful for things like:
- applying assertions to multiple properties of an object, for example for a field-by-field comparison.
- applying assertions to all elements of a collection or entries in a map, reporting on individual elements.
Imagine we're creating an assertion function that tests fails if any element of a collection is null
.
fun <T : Iterable<E?>, E> Assertion.Builder<T>.containsNoNullElements(): Assertion.Builder<T> =
compose("does not contain any null elements") { subject ->
subject.forEach { element ->
get("%s") { element }.isNotNull()
}
} then {
if (allPassed) pass() else fail()
}
Breaking this down:
- We declare the overall assertion function applies to an
Iterable
of a nullable element typeE
. - We use the
compose
method instead ofassert
. - Inside the
compose
block we make assertions about each element of the iterable subject. - Inside the
then
block we pass or fail the overall assertion depending on whether the nested assertions all passed.
The receiver of the block passed to result
has the properties allFailed
, anyFailed
, allPassed
and anyPassed
along with pass()
and fail()
functions used in simple assertions.
If the assertion failed we'll see something like this:
▼ Expect that ["catflap", null, "rubberplant", "marzipan"]:
✗ does not contain any null elements
▼ "catflap":
✓ is not null
▼ null:
✗ is not null
▼ "rubberplant":
✓ is not null
▼ "marzipan":
✓ is not null
As well as the overall assertion failure message we get a detailed breakdown allowing us to easily find exactly where the problem is.
Several assertion functions in Strikt's standard assertions library use nested assertions.
For example, Assertion.Builder<Iterable<E>>.all
applies assertions to each element of an Iterable
then passes the overall assertion if (and only if) all those nested assertions passed (allPassed
).
On the other hand Assertion.Builder<Iterable<E>>.any
applies assertions to the elements of an Iterable
but will pass the overall assertion if at least one of those nested assertions passed (anyPassed
).
The Assertion.Builder<Iterable<E>>.none
assertion passes only if allFailed
is true for its nested assertions!