Skip to main content
Version: 5.8

Composed Matchers

Composed matchers can be created for any type by composing one or more matchers. This allows to build up complex matchers from simpler ones. There are two logical operations, using which we can compose matchers: logical sum (Matcher.any) and logical product (Matcher.all).

Let's say we'd like to define a password Matcher, which will containADigit(), contain(Regex("[a-z]")) and contain(Regex("[A-Z]")). We can compose these matchers this way:

val passwordMatcher = Matcher.all(
containADigit(), contain(Regex("[a-z]")), contain(Regex("[A-Z]"))
)

We can add extension function then:

fun String.shouldBeStrongPassword() = this shouldBe passwordMatcher

So it can be invoked like this:

"StrongPassword123".shouldBeStrongPassword()
"WeakPassword".shouldBeStrongPassword() // would fail

By analogy, we can build a composed matcher using Matcher.any. In this case, passwordMatcher will fail only if all matchers fail, otherwise it will pass.

val passwordMatcher = Matcher.any(
containADigit(), contain(Regex("[a-z]")), contain(Regex("[A-Z]"))
)

Composed matchers can also be created for any class or interface by composing one or more other matchers along with the property to extract to test against.

For example, say we had the following structures:

data class Person(
val name: String,
val age: Int,
val address: Address,
)

data class Address(
val city: String,
val street: String,
val buildingNumber: String,
)

And our goal is to have a Person matcher that checks for people in Warsaw. We can define matchers for each of those components like this:

fun nameMatcher(name: String) = Matcher<String> {
MatcherResult(
value == name,
{ "Name $value should be $name" },
{ "Name $value should not be $name" }
)
}

fun ageMatcher(age: Int) = Matcher<Int> {
MatcherResult(
value == age,
{ "Age $value should be $age" },
{ "Age $value should not be $age" }
)
}

val addressMatcher = Matcher<Address> {
MatcherResult(
value == Address("Warsaw", "Test", "1/1"),
{ "Address $value should be Test 1/1 Warsaw" },
{ "Address $value should not be Test 1/1 Warsaw" }
)
}

Now we can simply combine these together to make a John in Warsaw matcher. Notice that we specify the property to extract to pass to each matcher in turn.

fun personMatcher(name: String, age: Int) = Matcher.all(
havingProperty(nameMatcher(name) to Person::name),
havingProperty(ageMatcher(age) to Person::age),
havingProperty(addressMatcher to Person::address)
)

And we can add the extension variant too:

fun Person.shouldBePerson(name: String, age: Int) = this shouldBe personMatcher(name, age)

Then we invoke it this way:

Person("John", 21, Address("Warsaw", "Test", "1/1")).shouldBePerson("John", 21)
Person("Sam", 22, Address("Chicago", "Test", "1/1")).shouldBePerson("John", 21) // would fail