Skip to main content

Core

The core module defines the MatcherField<T, R> interface and all extension functions that turn fields into matchers.

Artifact: dev.kensa:kensa-kotest-test-support

Source


MatcherField<T, R>

interface MatcherField<T, R> {
val name: String
fun extract(value: T): R?
val description: String
}
MemberDescription
nameIdentifier used as the basis for description.
extract(value: T): R?Pulls the field's value out of a subject of type T. Returns null when absent.
descriptionHuman-readable label used in failure messages. Derived from name by default.

Description derivation

The default description splits name on camelCase boundaries, drops any leading article (a, an, the), and drops a trailing Field token:

namedescription
aProviderCodeProvider Code
anAddressPostcodeAddress Postcode
aFooFieldFoo
firstProfileTypeFirst Profile Type

For JSON fields, description is overridden to return the JSONPointer path directly (e.g. /profiles/0/supplier), since the path is already unique and readable.


Extension functions

of

infix fun <T : Any, R> MatcherField<T, R>.of(expected: R?): Matcher<T?>

Equality check. The most common operator.

anAddressPostcode of fixtures[postcode]
serviceable of true
profileCount of 3

matching(matcher)

infix fun <T : Any, R> MatcherField<T, R>.matching(matcher: Matcher<R?>): Matcher<T?>

Delegates to any matcher from your chosen library.

anAmount matching beGreaterThan(0)
profileTypes matching containExactlyInAnyOrder("FTTC")

matching(matcher, vararg others)

fun <T : Any, R> MatcherField<T, R>.matching(matcher: Matcher<R?>, vararg others: Matcher<R?>): Matcher<T?>

Composes multiple matchers against the same field. All must pass.

Uses Matcher.all. Equivalent to chaining .and() but scoped to one field.

anAmount.matching(beGreaterThan(0), beLessThan(1000))

matching(expectedRegex) — String fields only

infix fun <T : Any> MatcherField<T, String>.matching(expectedRegex: String): Matcher<T?>

Matches the extracted string against a regular expression.

Applies Kotest's match(regex).

aSku matching "[A-Z]{3}-\\d+"

withListOf

fun <T : Any, R> MatcherField<T, List<R>>.withListOf(vararg expected: R): Matcher<T?>

Exact ordered list equality. The field must extract a List<R>.

profileDescriptions.withListOf("Full Fibre 900", "Full Fibre 500")

Uses Kotest containExactly.

withSetOf

fun <T : Any, R> MatcherField<T, Set<R>>.withSetOf(vararg expected: R): Matcher<T?>

Order-insensitive set equality. The field must extract a Set<R>.

supportedProtocols.withSetOf("FTTC", "FTTP")

Uses Kotest containExactlyInAnyOrder.


Phrasing sugar

These functions are in Phrases.kt alongside the core extension functions. They add no logic — they make assertion call-sites read like English.

with

infix fun <T> Matcher<T>.with(other: Matcher<T>): Matcher<T>

Composes two matchers (both must pass). An alternative to .and() / and when you want the call-site to read as a sentence.

(aProviderCode of "FW") with (aServiceType of "FTTP")

thatHas(matcher)

fun <T> thatHas(matcher: Matcher<T>): Matcher<T>

Identity wrapper. Use it to open a multi-line assertion block that reads like prose.

assertThat(response, thatHas(aProviderCode of "FW"))

thatHas(matcher, vararg others)

fun <T> thatHas(matcher: Matcher<T>, vararg matchers: Matcher<T>): Matcher<T>

Composes multiple matchers — all must pass.

Uses Matcher.all.

assertThat(response, thatHas(
aProviderCode of "FW",
aServiceType of "FTTP",
aDate of "2026-01-01"
))

thatIs

fun <T> thatIs(matcher: Matcher<T>): Matcher<T>

Identity wrapper, same as thatHas. Use whichever reads more naturally at the call-site.

assertThat(status, thatIs(aStatus of "SERVICEABLE"))

shouldHaveAll

fun <T> T?.shouldHaveAll(vararg matchers: Matcher<T?>)

Extension on the subject. Asserts that it passes every supplied matcher.

Calls should(Matcher.all(...)).

response.shouldHaveAll(
aProviderCode of "FW",
aServiceType of "FTTP"
)

Extension points

toMatcher

fun <T : Any, R, OUT> MatcherField<T, R>.toMatcher(expected: OUT?, convertActual: (R) -> OUT?): Matcher<T?>

Use when you have a domain value type and want a custom infix function instead of of. convertActual transforms the extracted value before the equality check, so expected can be a domain type different from R.

// Define once alongside the field:
infix fun <T : Any> MatcherField<T, ProviderCode>.withValue(expected: String) =
toMatcher(expected) { it.code }

// In tests:
aProviderCode withValue "FW"

nullableExtractingMatcher / extractingMatcher

The building block used internally by all extension functions. Exposed for writing field-aware matchers that do not fit the MatcherField interface.

fun <T : Any, R> nullableExtractingMatcher(
name: String,
extractValue: (T) -> R?,
match: Matcher<R?>
): Matcher<T?>

match receives null when the subject is null or extraction returns null. Extraction exceptions are surfaced with name in the message.