Matcher Architecture¶
expect(...).to.be(...) is built on top of a small Matcher role. The expected
value passed to .be(...) is either:
- a plain value — wrapped in the built-in
BeMatcher(smartmatch), or - a
Matcher-doing object — used directly.
This is the seam every built-in matcher and every user-defined matcher plugs into.
The Matcher role¶
1 2 3 4 5 6 7 8 9 | |
| Method | Required? | Purpose |
|---|---|---|
matches($actual) |
yes | Return True / False for whether $actual matches. |
failure-message($actual) |
no | Message rendered when the expectation fails (positive form). Default: undefined Str (falls back to Expected: / to be: rendering). |
failure-message-negated($actual) |
no | Message rendered when a .not expectation fails. Default: undefined Str. |
expected-value |
no | The value stored in Failure.expected for tooling. |
description |
no | Human-readable description, useful for error reporting and reflection. |
BeMatcher (built-in)¶
BeMatcher wraps Raku's smartmatch operator (~~). When you write:
1 2 3 4 | |
…the runner constructs BeMatcher.new(:expected(...)) under the hood. Because
BeMatcher deliberately leaves failure-message undefined, failure rendering
keeps the structured Expected: / to be: block plus the colorized Diff:
section described in Diff Output.
EqMatcher (built-in)¶
eq checks order-dependent structural equality using Raku's eqv operator.
It's invoked via expect(...).to.eq(...):
1 2 3 4 5 | |
eqv is type-strict, so an Array is not equivalent to a List with the same
elements:
1 | |
EqMatcher deliberately leaves failure-message undefined, so failures fall
through to the structured Expected: / to be: block plus the colorized
Diff: section described in Diff Output.
Negation works the usual way:
1 | |
ContainExactlyMatcher (built-in)¶
contain-exactly checks order-independent multiset equality on arrays / lists.
Each item in actual must correspond to one item in the expected list (matched
by eqv), with counts and totals matching:
1 2 3 4 5 | |
Items are passed as individual positional arguments. The slurp is
non-flattening, so passing a single array (contain-exactly([1, 2])) looks for
that array as one element. To spread an existing array, use |@arr:
1 2 | |
The empty form passes for an empty array:
1 | |
Failure messages render as expected <actual> to contain exactly <items> (or
not to contain exactly under .not).
match-array is the array-form alias — it takes a single array argument and
delegates to the same matcher:
1 2 | |
match-array requires its argument to be an array / list; passing a scalar
dies with match-array requires an array argument.
IncludeMatcher (built-in)¶
include checks membership across arrays, hashes, sets, bags, ranges, and
strings. It's invoked via expect(...).to.include(...):
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Multiple items are combined with AND semantics: every item must be present
for the matcher to pass. The slurp is non-flattening, so passing a single
array argument (include([1, 2])) looks for that array as one element rather
than spreading it. To spread an existing array, use |@arr.
Negation works the usual way:
1 | |
Failure messages render as expected <actual> to include <items> (or not
to include under .not).
StartWithMatcher (built-in)¶
start-with checks that a sequence (Array, List) begins with the supplied
items, or that a string begins with each supplied prefix:
1 2 3 4 5 6 7 8 9 | |
For Positional / Iterable actuals, the args form an in-order prefix matched
via eqv. For Str actuals, each arg must independently be a prefix of the
string (AND semantics).
The slurp is non-flattening, so passing a single array (start-with([1, 2]))
looks for that array as one prefix element. To spread an existing array, use
|@arr.
start-with rejects undefined or non-iterable, non-string actuals. Empty arg
lists die with start-with requires at least one item.
Failure messages render as expected <actual> to start with <items> (or not
to start with under .not).
EndWithMatcher (built-in)¶
end-with mirrors start-with for the trailing end of a sequence or string:
1 2 3 4 5 6 7 8 9 | |
Same slurp / type / empty-arg conventions as start-with. Failure messages
render as expected <actual> to end with <items> (or not to end with under
.not).
AllMatcher (built-in)¶
all checks that every element of a collection matches an inner matcher.
The inner argument is either a plain value (wrapped in BeMatcher, smartmatch
semantics) or any object that does Matcher:
1 2 3 4 5 6 7 8 9 | |
An empty collection passes vacuously:
1 2 | |
Undefined or non-iterable actuals fail with a shape failure message
(expected ... to be a collection ...). For sequence actuals, the matcher
iterates $actual.list, so Hash actuals are iterated as Pairs.
Failure messages render as
expected <actual> to all <inner-description> (element at index N: <item> did not match),
pointing at the first failing element. Negation renders as
expected <actual> not to all <inner-description>.
Composing across collections of collections¶
all is most useful when the inner matcher is itself a structural matcher:
1 2 3 4 5 6 | |
Junctions¶
Pass junctions through .all(...) directly — the method binds its argument
raw so junctions are not autothreaded out:
1 | |
BeAMatcher (built-in)¶
be-a checks whether the actual value is "of" a given type, including
subclasses, composed roles, and subset types. Internally it smartmatches
the actual value against the type ($actual ~~ $type):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
be-an is provided as an alias for English grammar — it behaves identically:
1 | |
Failure messages render as expected <actual> to be a <type> (or not to be
a <type> under .not). The type name comes from $type.^name.
BeAnInstanceOfMatcher (built-in)¶
be-an-instance-of is a strict type check: the actual value's runtime
type must be exactly the given type. Subclasses, composed roles, and subsets
do not match. Internally it checks $actual.defined && $actual.WHAT === $type:
1 2 3 4 | |
Type objects (the uninstantiated type itself) do not match:
1 | |
Because composed roles and subsets are not the runtime type of any concrete
object, be-an-instance-of(SomeRole) and be-an-instance-of(SomeSubset)
always fail. Use be-a for those cases.
Failure messages render as expected <actual> to be an instance of <type>
(or not to be an instance of <type> under .not).
RespondToMatcher (built-in)¶
respond-to checks whether the actual value has one or more methods.
Internally it uses the meta-object protocol's ^can introspection
($actual.^can($name)), so it works on both instances and type objects,
and recognises methods supplied by composed roles and parent classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Multiple method names are AND-combined — every name must be present for the expectation to pass. When the expectation fails, the failure message lists the missing methods:
1 | |
Negation works the usual way:
1 | |
Failure messages render as expected <actual> to respond to <names>
(or not to respond to <names> under .not).
HaveAttributesMatcher (built-in)¶
have-attributes checks several attributes of an object in one call.
Each named pair maps an attribute name to an expected value (or to
another Matcher). For each pair, the matcher calls the accessor on
the actual value and compares — values are compared with eqv, and
when the expected side is itself a Matcher its matches method is
delegated to.
1 2 3 4 5 6 7 8 9 10 | |
Multiple attributes are AND-combined — every pair must match. When the expectation fails, the failure message separates missing accessors (the object does not respond to the name at all) from mismatched values (accessor exists, but the value disagrees):
1 | |
The matcher composes naturally with other matchers — pass a Matcher
instance as the expected value for any attribute:
1 2 3 4 5 6 | |
Negation works the usual way:
1 | |
Failure messages render as expected <actual> to have attributes
<pairs> (or not to have attributes <pairs> under .not).
Comparison matchers (built-in)¶
Four matchers cover numeric ordering:
| Matcher | Operator | Aliases |
|---|---|---|
be-greater-than |
> |
be-gt |
be-greater-than-or-equal-to |
>= |
be-gte |
be-less-than |
< |
be-lt |
be-less-than-or-equal-to |
<= |
be-lte |
1 2 3 4 5 6 7 8 9 | |
All four accept any Real value, so Int, Rat, and Num mix freely
and negatives / zero behave as expected:
1 2 3 4 5 | |
Each matcher fails (rather than dying) when $actual is undefined or
is not a Real, so a stray Int-typed Nil or a Str produces a
recorded failure instead of a runtime error:
1 2 | |
Negation works the usual way:
1 2 | |
Failure messages render as expected <actual> to be greater than
<expected> (and the obvious variants for the other three), or not to
be … under .not.
Writing a custom matcher¶
Define a class that does Matcher and pass an instance to .be(...):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
When the matcher reports a failure:
- The matcher's
failure-message($actual)(orfailure-message-negated($actual)for.not) becomesFailure.messageand is what the failure summary prints. Failure.givenholds the actual value,Failure.expectedholdsexpected-value. Both are still useful for programmatic consumers and for alternate formatters.
Negation¶
.not flips the boolean result of matches before the framework decides
whether to record a failure. Matchers do not need to special-case negation;
they only need to return a sensible message from failure-message-negated for
when .not fails (i.e., the matcher matched but should not have).
Built-in matchers and user-supplied matchers go through the same role, so a
custom matcher you write integrates with expect exactly the way the
built-ins do.