matchers
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), ora
Matcher-doing object (used directly).
This is the seam every built-in matcher and every user-defined matcher plugs
into. If you want to add your own matcher without writing a class by hand, see
Custom Matchers. define-matcher produces a matcher
that conforms to this same role.
The Matcher role
unit module BDD::Behave::Matcher;
role Matcher is export {
method matches($actual --> Bool) { ... }
method failure-message($actual --> Str) { Str }
method failure-message-negated($actual --> Str) { Str }
method expected-value(--> Mu) { Nil }
method description(--> Str) { self.^name }
}| 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. |
Where matcher classes live
Almost all users only ever write use BDD::Behave;, which pulls in expect, every built-in matcher, and the full DSL through the lazy-loading wrapper. Nothing here changes that.
You only need to know about specific submodules when you want to instantiate matcher classes directly, typically to compose them inside a custom matcher, or to unit-test a matcher with Test. Each family lives in its own submodule so unit tests can import just one and skip the rest:
| Submodule | Matcher classes |
|---|---|
BDD::Behave::Matcher | The Matcher role itself. |
BDD::Behave::Matcher::Core | BeMatcher, EqMatcher |
BDD::Behave::Matcher::Collection | IncludeMatcher, ContainExactlyMatcher, StartWithMatcher, EndWithMatcher, AllMatcher |
BDD::Behave::Matcher::Type | BeAMatcher, BeAnInstanceOfMatcher, RespondToMatcher, HaveAttributesMatcher |
BDD::Behave::Matcher::Numeric | BeGreaterThanMatcher, BeGreaterThanOrEqualMatcher, BeLessThanMatcher, BeLessThanOrEqualMatcher, BeBetweenMatcher, BeWithinMatcher |
BDD::Behave::Matcher::Boolean | BeTruthyMatcher, BeFalsyMatcher, BeNilMatcher |
BDD::Behave::Matcher::String | MatchMatcher |
BDD::Behave::Matcher::Exception | RaiseErrorMatcher |
BDD::Behave::Matcher::Change | ChangeMatcher |
BDD::Behave::Matcher::Async | BeKeptMatcher, BeBrokenMatcher, CompleteWithinMatcher, EmitMatcher, EmitAtLeastMatcher, CompleteMatcher, EventuallyMatcher |
Each submodule already uses BDD::Behave::Matcher internally, but the role is not re-exported. If your code needs both, import both:
use BDD::Behave::Matcher; # for `does Matcher`
use BDD::Behave::Matcher::Numeric; # for BeBetweenMatcher, BeWithinMatcher, ...BeMatcher (built-in)
BeMatcher wraps Raku's smartmatch operator (~~). When you write:
expect(42).to.be(42);
expect('hello').to.be(/hell/);
expect(5).to.be(1..10);
expect($x).to.be(any(1, 2, 3));…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.
Because the underlying operator is ~~, all four Raku junction kinds (any,
all, one, none) compose with BeMatcher directly. See
Junctions.
EqMatcher (built-in)
eq checks order-dependent structural equality using Raku's eqv operator.
It's invoked via expect(...).to.eq(...):
expect([1, 2, 3]).to.eq([1, 2, 3]); # passes
expect([1, 2, 3]).to.eq([3, 2, 1]); # fails (order matters)
expect({ a => 1, b => 2 }).to.eq({ a => 1, b => 2 }); # passes
expect(42).to.eq(42); # passeseqv is type-strict, so an Array is not equivalent to a List with the same
elements:
expect([1, 2, 3]).to.eq((1, 2, 3)); # fails (Array vs List)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:
expect([1, 2, 3]).to.not.eq([3, 2, 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:
expect([1, 2, 3]).to.contain-exactly(3, 1, 2); # passes
expect([1, 1, 2]).to.contain-exactly(1, 2, 1); # passes (multiset)
expect([1, 1, 2]).to.contain-exactly(1, 2); # fails (counts differ)
expect([1, 2, 3]).to.contain-exactly(1, 2); # fails (extra in actual)
expect([1, 2]).to.contain-exactly(1, 2, 3); # fails (missing)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:
my @want = 1, 2, 3;
expect([3, 2, 1]).to.contain-exactly(|@want);The empty form passes for an empty array:
expect([]).to.contain-exactly();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:
expect([1, 2, 3]).to.match-array([3, 2, 1]); # passes
expect([1, 2, 3]).to.match-array([1, 2]); # failsmatch-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(...):
expect([1, 2, 3]).to.include(2); # array element
expect([1, 2, 3]).to.include(1, 3); # multiple elements (AND)
expect([[1, 2], [3, 4]]).to.include([1, 2]); # nested element via eqv
expect({ a => 1, b => 2 }).to.include('a'); # hash key
expect({ a => 1, b => 2 }).to.include(a => 1); # hash key + value
expect({ a => 1, b => 2 }).to.include(:a(1)); # named-pair shorthand
expect('hello world').to.include('world'); # string substring
expect('hello world').to.include('hello', 'world');
expect(set('a', 'b')).to.include('a'); # Set / Bag membership
expect(1..10).to.include(5); # Range membershipMultiple 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:
expect([1, 2, 3]).to.not.include(99);Failure messages render as expected <actual> to include <items> (or not
to include under .not).
String-specific behavior
When the actual is a Str, include does substring matching via
.contains. A few details worth knowing:
expect('').to.include(''); # empty needle matches everything
expect("col1\tcol2").to.include("\t"); # whitespace and control chars
expect("a\nb").to.include("a\nb"); # substrings spanning newlines
expect('café au lait').to.include('café'); # Unicode (ASCII extension)
expect('日本語のテスト').to.include('日本'); # CJK
expect('done 🚀').to.include('🚀'); # emoji
expect('order #42').to.include(42); # non-Str args are .Str-coercedMatching is case sensitive: expect('Hello').to.include('hello') fails.
For case-insensitive matching, use MatchMatcher
with a regex like rx:i/hello/.
Allomorphs (IntStr, RatStr, <42>) are accepted as actuals and routed
through the Str branch since they smartmatch as Str.
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:
expect([1, 2, 3]).to.start-with(1); # passes
expect([1, 2, 3]).to.start-with(1, 2); # passes (in-order prefix)
expect([1, 2, 3]).to.start-with(2); # fails
expect([1, 2, 3]).to.start-with(2, 1); # fails (out of order)
expect([1]).to.start-with(1, 2); # fails (prefix longer)
expect('hello world').to.start-with('hello'); # passes
expect('hello world').to.start-with('hello', 'h'); # passes (each prefix AND)
expect('hello world').to.start-with('hello', 'world');# fails ('world' is not a prefix)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).
String-specific behavior
When the actual is a Str, each prefix is checked via .starts-with after
.Str coercion. Multi-prefix calls AND together: every supplied prefix must
independently be a prefix of the string:
expect('').to.start-with(''); # empty prefix matches everything
expect("\thello").to.start-with("\t"); # leading tab / control char
expect("a\nb").to.start-with("a\nb"); # prefix can cross a newline
expect('café au lait').to.start-with('café'); # Unicode (ASCII extension)
expect('日本語').to.start-with('日本'); # CJK
expect('🚀 launch').to.start-with('🚀'); # emoji
expect('42 answers').to.start-with(42); # non-Str args are .Str-coercedMatching is case sensitive and respects leading whitespace:
expect('hello').to.start-with(' hello') fails. Allomorphs (IntStr,
RatStr) are accepted as actuals and routed through the Str branch.
EndWithMatcher (built-in)
end-with mirrors start-with for the trailing end of a sequence or string:
expect([1, 2, 3]).to.end-with(3); # passes
expect([1, 2, 3]).to.end-with(2, 3); # passes (in-order suffix)
expect([1, 2, 3]).to.end-with(2); # fails
expect([1, 2, 3]).to.end-with(3, 2); # fails (out of order)
expect([1]).to.end-with(1, 2); # fails (suffix longer)
expect('hello world').to.end-with('world'); # passes
expect('hello world').to.end-with('world', 'd'); # passes (each suffix AND)
expect('hello world').to.end-with('world', 'hello'); # fails ('hello' is not a suffix)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).
String-specific behavior
When the actual is a Str, each suffix is checked via .ends-with after
.Str coercion. Multi-suffix calls AND together: every supplied suffix must
independently be a suffix of the string:
expect('').to.end-with(''); # empty suffix matches everything
expect("done\n").to.end-with("\n"); # trailing newline / control char
expect("a\nb").to.end-with("a\nb"); # suffix can cross a newline
expect('le café').to.end-with('café'); # Unicode (ASCII extension)
expect('日本語').to.end-with('語'); # CJK
expect('moon 🚀').to.end-with('🚀'); # emoji
expect('answer = 42').to.end-with(42); # non-Str args are .Str-coercedMatching is case sensitive and respects trailing whitespace:
expect('hello').to.end-with('hello ') fails. Allomorphs (IntStr, RatStr)
are accepted as actuals and routed through the Str branch.
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:
expect([1, 1, 1]).to.all(1); # plain value via BeMatcher
expect([1, 2, 3]).to.all(Int); # type
expect([1, 5, 10]).to.all(1..10); # range
expect(['foo', 'food']).to.all(/^foo/); # regex
expect([1, 2, 3]).to.all(PositiveMatcher.new); # custom matcher
expect([[1, 2], [1, 3]]).to.all( # composes with built-ins
StartWithMatcher.new(:expected([1]))
);An empty collection passes vacuously:
expect([]).to.all(Int); # passes
expect(()).to.all(1); # passesUndefined 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:
my @rows = [
{ id => 1, status => 'ok' },
{ id => 2, status => 'ok' },
];
expect(@rows).to.all(IncludeMatcher.new(:expected([status => 'ok'])));Junctions
Pass junctions through .all(...) directly. The method binds its argument
raw so junctions are not autothreaded out:
expect([1, 2, 3]).to.all(any(1, 2, 3));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):
expect(42).to.be-a(Int); # passes
expect(42).to.be-a(Numeric); # passes (Int is Numeric)
expect('hi').to.be-a(Int); # fails
class Animal {}
class Dog is Animal {}
expect(Dog.new).to.be-a(Animal); # passes (subclass)
role Walkable { method walk { } }
class Bird does Walkable {}
expect(Bird.new).to.be-a(Walkable); # passes (role composition)
subset Positive of Int where * > 0;
expect(5).to.be-a(Positive); # passes
expect(-1).to.be-a(Positive); # fails (where clause)be-an is provided as an alias for English grammar. It behaves identically:
expect(42).to.be-an(Int);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:
expect(Dog.new).to.be-an-instance-of(Dog); # passes
expect(Dog.new).to.be-an-instance-of(Animal); # fails (subclass)
expect(42).to.be-an-instance-of(Int); # passes
expect(42).to.be-an-instance-of(Numeric); # fails (parent role)Type objects (the uninstantiated type itself) do not match:
expect(Int).to.be-an-instance-of(Int); # fails (Int is undefined)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.
class Calculator {
method add($a, $b) { $a + $b }
method subtract($a, $b) { $a - $b }
}
expect(Calculator.new).to.respond-to('add'); # passes
expect(Calculator.new).to.respond-to('add', 'subtract'); # passes
expect(Calculator.new).to.respond-to('multiply'); # fails
role Greeter { method greet { 'hello' } }
class Person does Greeter { }
expect(Person.new).to.respond-to('greet'); # passes (via role)
expect('hello').to.respond-to('uc', 'lc', 'chars'); # built-ins work
expect([1, 2, 3]).to.respond-to('push', 'pop'); # Arrays tooMultiple 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:
expected Dog.new to respond to "bark", "meow", "purr" (missing: "meow", "purr")Negation works the usual way:
expect(Dog.new).to.not.respond-to('meow');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.
class Person {
has Str $.name;
has Int $.age;
}
my $alice = Person.new(:name<Alice>, :age(30));
expect($alice).to.have-attributes(:name<Alice>, :age(30)); # passes
expect($alice).to.have-attributes(:name<Alice>); # subset OK
expect($alice).to.have-attributes(:age(31)); # failsMultiple 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):
expected Person.new(name => "Alice", age => 30) to have attributes age => 31, nickname => "Al" (missing: "nickname"; mismatched: age: got 30, wanted 31)The matcher composes naturally with other matchers. Pass a Matcher
instance as the expected value for any attribute:
use BDD::Behave::Matcher::Collection; # StartWithMatcher
use BDD::Behave::Matcher::Type; # BeAMatcher
expect($alice).to.have-attributes(
:name(StartWithMatcher.new(:expected(['A']))),
:age(BeAMatcher.new(:type(Int))),
);Negation works the usual way:
expect($alice).to.not.have-attributes(:age(31));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 |
expect(5).to.be-greater-than(3);
expect(5).to.be-greater-than-or-equal-to(5);
expect(3).to.be-less-than(5);
expect(5).to.be-less-than-or-equal-to(5);
expect(5).to.be-gt(3);
expect(5).to.be-gte(5);
expect(3).to.be-lt(5);
expect(5).to.be-lte(5);All four accept any Real value, so Int, Rat, and Num mix freely
and negatives / zero behave as expected:
expect(1.5).to.be-greater-than(1.4); # Rat vs Rat
expect(5).to.be-greater-than(4.99); # Int vs Rat
expect(3.14e0).to.be-greater-than(3.0e0); # Num vs Num
expect(-1).to.be-greater-than(-5);
expect(0).to.not.be-greater-than(0);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:
expect(Int).to.be-greater-than(0); # records a failure
expect('abc').to.be-less-than(10); # records a failureNegation works the usual way:
expect(3).to.not.be-greater-than(5);
expect(7).to.not.be-less-than(5);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.
BeBetweenMatcher (built-in)
be-between checks whether a numeric actual falls within a [min, max]
range. The matcher is inclusive by default. Chain .exclusive (or the
explicit .inclusive) to flip the mode:
expect(5).to.be-between(1, 10); # inclusive (default), passes
expect(1).to.be-between(1, 10); # inclusive: lower bound passes
expect(10).to.be-between(1, 10); # inclusive: upper bound passes
expect(5).to.be-between(1, 10).exclusive; # exclusive: passes strictly inside
expect(1).to.be-between(1, 10).exclusive; # exclusive: lower bound fails
expect(10).to.be-between(1, 10).exclusive; # exclusive: upper bound fails
expect(5).to.be-between(1, 10).inclusive; # equivalent to the defaultAll four call sites use the same chainable expectation. Re-chaining a
modifier replaces any previously recorded failure, so
expect(10).to.be-between(1, 10).exclusive.inclusive ends with no
failure: the .exclusive step pushes one, then .inclusive clears it
when the re-evaluation passes.
be-between accepts any Real actual, so Int, Rat, and Num mix
freely and negative / zero bounds behave the obvious way:
expect(1.5).to.be-between(1.0, 2.0);
expect(2).to.be-between(1.5, 2.5);
expect(-3).to.be-between(-5, -1);
expect(0).to.be-between(0, 0);The 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:
expect(Int).to.be-between(0, 10); # records a failure
expect('abc').to.be-between(0, 10); # records a failureNegation works the usual way, and composes with the inclusive / exclusive modifiers:
expect(0).to.not.be-between(1, 10);
expect(1).to.not.be-between(1, 10).exclusive;Failure messages name both bounds and the active mode:
expected 11 to be between 1 and 10 (inclusive)
expected 10 to be between 1 and 10 (exclusive)Failure.expected is populated as [min, max] so programmatic
consumers and alternate formatters can introspect the bounds.
BeWithinMatcher (built-in)
be-within performs a tolerance check on a numeric actual against an
expected target, parameterized by a delta. The call shape uses an
.of(...) continuation so the delta and expected target read naturally:
expect(5.05).to.be-within(0.1).of(5.0); # passes (|5.05 - 5.0| <= 0.1)
expect(5.1).to.be-within(0.1).of(5.0); # passes (boundary is inclusive)
expect(5.2).to.be-within(0.1).of(5.0); # fails
expect(3.14e0).to.be-within(0.01e0).of(3.15e0); # Num toleranceThe boundary is inclusive (abs(actual - expected) <= delta), so a
delta of 0 means actual must equal expected exactly:
expect(5).to.be-within(0).of(5); # passes
expect(5.0001).to.be-within(0).of(5); # failsbe-within accepts any Real actual and target, so Int, Rat, and
Num mix freely. Negative values and zero behave the obvious way:
expect(-5.05).to.be-within(0.1).of(-5.0);
expect(0.05).to.be-within(0.1).of(0);The matcher fails (rather than dying) when either $actual or the
target passed to .of(...) is undefined or non-Real, so a stray
Int-typed Nil or a Str produces a recorded failure instead of a
runtime error:
expect(Int).to.be-within(0.1).of(5.0); # records a failure
expect('abc').to.be-within(0.1).of(5.0); # records a failure
expect(5.0).to.be-within(0.1).of(Int); # records a failureNegation works the usual way:
expect(5.2).to.not.be-within(0.1).of(5.0); # passes (outside tolerance)
expect(5.05).to.not.be-within(0.1).of(5.0); # fails (inside tolerance)Failure messages render the delta and expected target:
expected 5.2 to be within 0.1 of 5.0
expected 5.05 not to be within 0.1 of 5.0Failure.expected is populated with the expected target (not the
delta), and Failure.given holds the actual value, so programmatic
consumers and alternate formatters can introspect both.
BeTruthyMatcher (built-in)
be-truthy checks Raku's boolean coercion of the actual value (?$actual).
Anything that coerces to True passes. Anything that coerces to False
fails:
expect(True).to.be-truthy;
expect(1).to.be-truthy;
expect('hello').to.be-truthy;
expect([1, 2, 3]).to.be-truthy;
expect({ a => 1 }).to.be-truthy;
expect(False).to.not.be-truthy;
expect(0).to.not.be-truthy;
expect('').to.not.be-truthy;
expect([]).to.not.be-truthy;
expect(Nil).to.not.be-truthy;
expect(Int).to.not.be-truthy; # undefined type objectRaku's coercion may surprise users coming from Perl: non-empty strings
are always truthy ('0'.Bool is True), but an empty Array / Hash
is False:
expect('0').to.be-truthy; # non-empty string in Raku
expect([]).to.not.be-truthy;
expect({}).to.not.be-truthy;be-truthy takes no arguments. Negation works the usual way:
expect(0).to.not.be-truthy;Failure messages render as expected <actual> to be truthy (or not to
be truthy under .not).
BeFalsyMatcher (built-in)
be-falsy is the inverse of be-truthy. It passes when !$actual is
True:
expect(False).to.be-falsy;
expect(0).to.be-falsy;
expect('').to.be-falsy;
expect([]).to.be-falsy;
expect({}).to.be-falsy;
expect(Nil).to.be-falsy;
expect(Int).to.be-falsy; # undefined type object
expect(True).to.not.be-falsy;
expect(1).to.not.be-falsy;
expect('hello').to.not.be-falsy;
expect([1, 2, 3]).to.not.be-falsy;be-falsy takes no arguments. Failure messages render as
expected <actual> to be falsy (or not to be falsy under .not).
BeNilMatcher (built-in)
be-nil passes when the actual value is undefined (!$actual.defined).
That covers Raku's three flavors of "not a value":
NilitselfAny(the default for an unassigned scalar)any type object, including built-ins like
Int/Strand user-defined classes
expect(Nil).to.be-nil;
expect(Any).to.be-nil;
expect(Int).to.be-nil; # undefined built-in type object
expect(Str).to.be-nil;
my class Widget {}
expect(Widget).to.be-nil; # user-defined type object
expect(Widget.new).to.not.be-nil; # defined instanceDefined values (even "empty" or falsy ones) fail be-nil:
expect(0).to.not.be-nil;
expect('').to.not.be-nil;
expect([]).to.not.be-nil;
expect({}).to.not.be-nil;
expect(False).to.not.be-nil;This is the type-object-vs-instance distinction that Raku makes
explicit: Int (the type) is undefined, but 0 (an instance of
Int) is defined. Use be-nil when you care about "is this a real
value", and be-falsy when you care about boolean coercion (which
treats empty collections as false too).
Note that assigning Nil to a plain my $x reverts $x to Any, and
both still pass be-nil, so the distinction rarely matters in
practice.
be-nil takes no arguments. Failure messages render as
expected <actual.raku> to be nil (or not to be nil under .not).
MatchMatcher (built-in)
match passes when a Str actual value smartmatches against a Regex
expected value ($actual ~~ /pattern/). It accepts the full Raku regex
syntax, including character classes, alternation, anchors, modifiers,
and rx//-quoted forms.
expect('abc123').to.match(/\d+/);
expect('hello world').to.match(/world/);
expect('HELLO').to.match(rx:i/hello/); # case-insensitive
expect('hello').to.match(/^hello$/); # anchoredNegation uses the same .not chain:
expect('abc').to.not.match(/\d+/);
expect('cat').to.not.match(/dog/);Undefined and non-Str actuals fail rather than throw: expect(Any),
expect(Str), expect(42), and expect([1, 2, 3]) all record a
failure when matched against any regex, so a stray nil or wrongly typed
value produces a normal expectation failure instead of a runtime
exception.
Failure messages render the actual value and the regex via .raku:
expected "abc" to match /\d+/
expected "abc123" not to match /\d+/Failure.given carries the original string and Failure.expected
carries the Regex itself, so programmatic consumers and alternate
formatters can inspect either side.
match is regex-only. For substring checks use include (see
Matchers › IncludeMatcher).
RaiseErrorMatcher (built-in)
raise-error passes when the actual value is a Callable and invoking it
raises an exception. Because the matcher operates on a Callable, the
block must be passed unevaluated. Wrap the code under test in { ... }:
expect({ die "boom" }).to.raise-error;
expect({ X::AdHoc.new(payload => 'x').throw }).to.raise-error;
expect({ 1 + 1 }).to.not.raise-error;raise-error is also available under two aliases (throw and raise),
which take the same arguments and behave identically. Pick whichever
reads best at the call site:
expect({ die "boom" }).to.throw;
expect({ X::Demo.new.throw }).to.throw(X::Demo, /'demo'/);
expect({ 1 + 1 }).to.not.raise;
expect({ die "boom" }).to.raise.with-message('boom');Filtering by exception type
Pass an exception type as the first argument to require that the raised
exception is that type, a subclass of it, or a class that does it as a
role (matching uses smartmatch, i.e. $exception ~~ $type):
class X::Demo is Exception { method message { 'demo' } }
class X::Sub is X::Demo { }
expect({ X::Demo.new.throw }).to.raise-error(X::Demo); # passes
expect({ X::Sub.new.throw }).to.raise-error(X::Demo); # passes (subclass)
expect({ die "boom" }).to.raise-error(X::Demo); # failsWhen the block raises a different type, the failure surfaces what actually happened:
expected block to raise X::Demo, but raised X::AdHoc: boomWhen the block does not raise at all under a typed expectation:
expected block to raise X::Demo, but none was raisedFailure.expected carries the expected type, so programmatic consumers
and alternate formatters can read it directly.
Filtering by message pattern
Pass a Regex (with or without a leading type) to require that the
raised exception's .message matches the regex:
expect({ die "code=42" }).to.raise-error(/'code=42'/);
expect({ X::Demo.new.throw }).to.raise-error(X::Demo, /demo/);
expect({ X::Demo.new.throw }).to.raise-error(X::Demo, /nope/); # failsWhen the type matches but the message does not (or vice versa), the failure says what was raised so the mismatch is diagnosable inline:
expected block to raise X::Demo with message matching /nope/,
but raised X::Demo: demoFiltering by message via with-message
raise-error also supports a chained .with-message(...) form that
filters by either an exact Str or a Regex. The chain is equivalent
to passing the regex inline but reads more naturally when the message
constraint is a string:
expect({ die "boom" }).to.raise-error.with-message('boom'); # exact
expect({ die "code=42" }).to.raise-error.with-message(/'code=42'/); # regex
expect({ X::Demo.new.throw }).to.raise-error(X::Demo).with-message('demo');Str arguments compare with eq (the full message must match
exactly). Regex arguments compare with ~~. The chain may be
re-applied: only the most recent call's expectation is in effect, and
any failure recorded by an earlier link in the chain is replaced rather
than appended:
my $exp = expect({ die "boom" }).to.raise-error.with-message('bang');
# Failures.list has one entry.
$exp.with-message('boom');
# Failures.list is now empty. The second call succeeded and cleared
# the prior failure.When the chain ultimately fails, the failure message reflects the final constraint:
expected block to raise an error with message "bang", but raised X::AdHoc: boom
expected block to raise X::Demo with message "demo", but raised X::AdHoc: oops.with-message also composes with .not:
expect({ die "boom" }).to.not.raise-error.with-message('bang'); # passes
expect({ die "boom" }).to.not.raise-error.with-message('boom'); # failsNegation
.not.raise-error(...) passes whenever the block fails to match the
full filter: i.e. nothing was raised, or a different type was raised,
or the message did not match. It only fails when the block raised
exactly the forbidden exception:
expect({ die "boom" }).to.not.raise-error(X::Demo); # passes (other type)
expect({ 1 + 1 }).to.not.raise-error(X::Demo); # passes (no raise)
expect({ X::Demo.new.throw }).to.not.raise-error(X::Demo); # failsWhen .not.raise-error fails, the negated failure includes the raised
exception's type and message so you can diagnose without re-running:
expected block not to raise an error, but one was raised (X::AdHoc: boom)
expected block not to raise X::Demo, but one was raised (X::Demo: demo)Non-Callable actuals
Non-Callable actuals fail rather than throw: expect(42).to.raise-error
records a normal expectation failure rather than blowing up. The failure
message names the unwrapped value so the mistake is obvious:
expected a Callable, but got 42Failure metadata
Failure.given carries the original Callable. Failure.expected
carries the expected exception type (when one was supplied), or the
expected message (regex or Str, whichever was supplied via the inline
form or with-message). For the bare no-argument form it remains
unset. The matcher itself also exposes raised-exception (the captured
throw) and miss-reason (one of 'none', 'type', 'message',
'non-callable', or Str on a pass) for tooling that needs structural
access.
ChangeMatcher (built-in)
change passes when invoking the actual block changes the value
returned by an observable block. Both the action and the observable
are passed unevaluated as { ... } blocks. The matcher invokes the
observable, runs the action, then invokes the observable again and
compares the two values via eqv:
my $counter = 0;
expect({ $counter++ }).to.change({ $counter }); # passes
my @items;
expect({ @items.push: 'x' }).to.change({ @items.elems }); # passes
my $counter2 = 5;
expect({ 1 + 1 }).to.change({ $counter2 }); # fails (unchanged)The observable can be any expression (a variable, a method call, a
derived value) as long as the block returns something comparable. The
matcher uses eqv for the before / after comparison, so deep
structural equality is respected: mutating an array element to the
same value (@items[0] = 1 when it was already 1) does not
register as a change.
Capturing snapshots
Because eqv compares the values returned by the observable each
time, an observable like { @items } returns the same container on
both calls. To detect element-level mutation, snapshot the value:
my @items = 1, 2, 3;
expect({ @items.push: 4 }).to.change({ @items.clone }); # passes
expect({ @items.push: 5 }).to.change({ @items.elems }); # passes (count)Negation
.not.change passes when the action does not change the
observable, and fails when it does:
my $counter = 5;
expect({ 1 + 1 }).to.not.change({ $counter }); # passes
expect({ $counter = 9 }).to.not.change({ $counter });
# fails: expected block not to change observable, but it changed from 5 to 9Non-Callable actuals
Non-Callable actuals fail rather than throw: expect(42).to.change({ $x })
records a normal expectation failure rather than blowing up. The
failure message names the unwrapped value:
expected a Callable for change, but got 42Filtering with .from and .to
change returns a chainable expectation that can be narrowed with
.from(value) and .to(value) to assert the precise transition:
my $counter = 0;
expect({ $counter = 10 }).to.change({ $counter }).from(0).to(10);
# .from alone: must start at 0 *and* change
my $a = 0;
expect({ $a++ }).to.change({ $a }).from(0);
# .to alone: must end at 10 *and* differ from the start
my $b = 0;
expect({ $b = 10 }).to.change({ $b }).to(10);
# .to.from order is interchangeable
my $c = 1;
expect({ $c = 2 }).to.change({ $c }).to(2).from(1);.from and .to use eqv for the comparison, so deep structural
equality is respected (e.g. .from([]) matches an empty-array
snapshot). The action block is run exactly once per .change(...)
call. Chaining .from(...).to(...) after a .change(...) does
not re-run it.
When a chain step fails and a later step passes, the prior failure
is replaced (rather than duplicated) so the recorded Failure list
always reflects the final state.
Negation with .from / .to
The chain participates in the existing .not flow with conjunction
semantics: .not.change(...).from(x).to(y) passes when the joint
constraint (change AND from match AND to match) does not hold.
This means the block may still have changed the observable, just
not in that particular direction. The negated failure message
includes the clause for clarity:
my $counter = 0;
expect({ $counter = 7 }).to.not.change({ $counter }).from(0).to(10);
# passes: the value changed, but not to 10
expect({ $counter = 10 }).to.not.change({ $counter }).from(0).to(10);
# fails: expected block not to change observable from 0 to 10,
# but it changed from 0 to 10Filtering with .by / .by-at-least / .by-at-most
For numeric observables, .by(delta) asserts the precise signed
change, while .by-at-least(delta) and .by-at-most(delta) bound
the change from below and above:
my $counter = 0;
expect({ $counter += 5 }).to.change({ $counter }).by(5);
# negative delta
my $balance = 100;
expect({ $balance -= 25 }).to.change({ $balance }).by(-25);
# bounded range
my $score = 0;
expect({ $score += 5 }).to.change({ $score })
.by-at-least(1).by-at-most(10);All three accept any Real (Int, Rat, Num). The matcher
subtracts after - before and compares against the expected delta
with the usual numeric operators. If the observable returns a
non-numeric value (e.g. a Str), the matcher records a
by-non-numeric failure rather than coercing.
The by-modifiers compose with .from(...) and .to(...). From
and to mismatches still take priority: .from(0).by(5) reports
a from-mismatch when the value did not start at 0, even if the
delta would otherwise be wrong. RSpec parity: when the value did
not change at all, the no-change miss-reason fires first, so
.by(0) is effectively unreachable.
When a chain step fails and a later step passes, the prior
failure is replaced (rather than duplicated) so the recorded
Failure list always reflects the final state.
Failure messages
expected block to change observable, but it remained 7
expected block not to change observable, but it changed from 0 to 9
expected block to change observable from 0, but it started as 3
expected block to change observable to 10, but it ended as 7
expected block to change observable from 0 to 10, but it started as 3
expected block to change observable from 0 to 10, but it ended as 7
expected block to change observable from 5 to 5, but it remained 5
expected block to change observable by 5, but it changed by 3
expected block to change observable by at least 5, but it changed by 3
expected block to change observable by at most 5, but it changed by 7
expected a Callable for change, but got 42Failure metadata
Failure.given carries the original action Callable.
Failure.expected reflects the most-specific constraint
supplied: [from, to] when both endpoints are set, the from / to
value alone when only one is set, the by / by-at-least /
by-at-most value when only a delta constraint is set, or Nil
for the bare form. The matcher itself exposes before-value,
after-value, action-ran, callable-given, and miss-reason
(one of 'non-callable', 'from', 'to', 'no-change',
'by', 'by-at-least', 'by-at-most', 'by-non-numeric', or
Str on a pass) for tooling that needs structural access to the
captured states. delta returns after - before when both
values are Real, and Nil otherwise.
BeKeptMatcher (built-in)
be-kept waits for a Promise to settle and passes when it ends in the
Kept state. It blocks up to a configurable timeout (default 5 seconds)
using Promise.anyof(...) so it never hangs forever:
expect(Promise.kept('done')).to.be-kept; # passes immediately
expect(start { compute() }).to.be-kept; # passes when start { } finishes
expect(Promise.broken('oops')).to.not.be-kept; # passes; broken is not kept
expect($pending).to.be-kept(0.5); # custom timeout in secondsIf the promise broke, the failure message includes the cause's class and message so you can see what went wrong without rerunning:
expected Promise to be kept, but it was broken (X::AdHoc: boom)If the timeout elapsed with the promise still pending, the message quotes the timeout:
expected Promise to be kept within 0.05s, but it was still pendingThe matcher exposes value (set when the promise was kept),
cause (set when the promise was broken), timed-out, and
promise-given for tooling.
be-kept accepts a positional Real timeout in seconds. Pass 0.5
for 500 ms. Non-Promise actuals fail with a Promise-shape message
(expected a Promise for be-kept, but got 42) rather than dying.
BeBrokenMatcher (built-in)
be-broken is the mirror of be-kept: it waits up to the timeout
(default 5 seconds) and passes when the promise ends in the
Broken state. The captured cause is exposed on the matcher's
cause accessor:
expect(Promise.broken('boom')).to.be-broken;
expect(start { die 'eventual failure' }).to.be-broken;
expect(Promise.kept('happy')).to.not.be-broken;
expect($pending).to.be-broken(0.5); # custom timeoutWhen the promise was kept instead, the failure message includes the kept value:
expected Promise to be broken, but it was kept with "happy"When the promise was still pending after the timeout:
expected Promise to be broken within 0.05s, but it was still pendingThe negated form surfaces the broken cause so you can see what broke when you expected it not to:
expected Promise not to be broken, but it was (X::AdHoc: boom)The matcher exposes value (set when the promise was kept), cause
(set when broken), was-kept, timed-out, and promise-given.
CompleteWithinMatcher (built-in)
complete-within passes when the promise settles (either kept or
broken) within the given duration. The duration is a Real
positional argument in seconds:
expect(Promise.kept('done')).to.complete-within(1);
expect(Promise.broken('boom')).to.complete-within(1);
expect(start { sleep 0.01; 42 }).to.complete-within(1);
expect($pending).to.not.complete-within(0.05);The failure message when the promise is still pending:
expected Promise to complete within 0.05s, but it was still pendingThe negated form indicates the final status:
expected Promise not to complete within 1s, but it was kept with "done"
expected Promise not to complete within 1s, but it was broken (X::AdHoc: boom)Failure.expected carries the duration so failure metadata is
queryable from tooling. The matcher exposes final-status (Kept,
Broken, or Nil when timed out), value, cause, timed-out,
and promise-given.
Unlike be-kept / be-broken, the duration is required. There's
no useful default for "how long is too long" without it.
EmitMatcher (built-in)
emit taps a Supply or iterates a Channel and passes when the
source emits exactly the expected sequence of values within a
configurable collection window. Comparison is element-by-element via
eqv:
expect(Supply.from-list(1, 2, 3)).to.emit(1, 2, 3);
expect(Supply.from-list('a', 'b')).to.emit('a', 'b');
my $ch = Channel.new;
$ch.send(1); $ch.send(2); $ch.close;
expect($ch).to.emit(1, 2);
expect(Supply.from-list(1, 2)).to.not.emit(1, 2, 3);The collection window defaults to 1 second. Pass :within(seconds) to
change it, useful for slow streams or to fail fast on hung supplies:
expect(Supply.from-list(1, 2)).to.emit(1, 2, :within(0.5));emit uses react/whenever under the hood and stops collecting as
soon as the expected number of values arrive (the window is only the
maximum wait). This makes the matcher fast for known-length sequences.
Failure messages render both the expected and the actually-emitted sequences:
expected stream to emit [1, 2, 3] within 1s, but it emitted [1, 2]Non-stream actuals (anything that's neither Supply nor Channel)
record a shape-failure rather than dying:
expected a Supply or Channel for emit, but got 42Failure.expected carries the expected values list for tooling.
EmitAtLeastMatcher (built-in)
emit-at-least passes when a Supply or Channel emits at least the
given number of values within the collection window. The matcher stops
collecting as soon as the threshold is reached:
expect(Supply.from-list(1, 2, 3, 4)).to.emit-at-least(2);
expect(Supply.from-list('a')).to.not.emit-at-least(3, :within(0.1));The :within window defaults to 1 second.
Failure messages report the count that was actually emitted:
expected stream to emit at least 3 values within 0.1s, but it emitted 1Failure.expected carries the minimum count.
CompleteMatcher (built-in)
complete waits for a Supply to send done (or for a Channel to
be closed) within the collection window:
expect(Supply.from-list(1, 2, 3)).to.complete;
my $ch = Channel.new;
$ch.send(1); $ch.close;
expect($ch).to.complete;
my $supplier = Supplier.new;
expect($supplier.Supply).to.not.complete(:within(0.1));The :within window defaults to 1 second.
Failure messages report whether the stream was still active or had errored:
expected stream to complete within 0.1s, but it was still active (emitted 0 values)
expected stream to complete within 1s, but it quit (X::AdHoc: boom)Failure.expected carries the window so failure metadata is queryable
from tooling.
Unlike Promise's complete-within, the complete matcher does not
treat a stream-level error (QUIT) as completion. Only the done
signal counts.
EventuallyMatcher (built-in)
eventually re-runs a Callable block on a polling loop until the
inner matcher passes or the timeout elapses. Useful for asserting on
eventually-consistent state without hand-rolled polling:
expect({ get-job-status() }).to.eventually.be('done');
expect({ counter() }).to.eventually(:timeout(5)).be-greater-than(100);
expect({ log-buffer() }).to.eventually.match(/'ready'/);The chained matcher methods (.be, .eq, .match, .include,
.be-truthy, .be-a, .be-greater-than, etc.) build the appropriate
inner matcher and feed each polled value through it. For matchers
outside the built-in chained set, pass a Matcher instance through
.matches-with:
expect({ load() }).to.eventually.matches-with(MyCustomMatcher.new(...));The eventually(...) call takes two named arguments:
| Name | Default | Meaning |
|---|---|---|
:timeout | 2.0 | Max seconds to wait for a match |
:interval | 0.05 | Seconds to sleep between iterations |
Polling exits as soon as the inner matcher passes, so a passing assertion completes quickly. A miss runs out the full timeout before recording a failure.
Failure messages report the iteration count and the elapsed wall-clock time:
eventually: did not match be 0 (after 5 iterations in 0.05s)If the block throws on every iteration, the failure message names the last exception:
eventually: block threw X::AdHoc: not yet (after 5 iterations in 0.05s)Exceptions raised inside the block are caught per-iteration and treated as misses. The polling loop continues so the block has a chance to recover (e.g., a service warming up).
Negation
Under .not, eventually passes only when the inner matcher never
matched for the full timeout window:
expect({ get-status() }).to.not.eventually(:timeout(0.1)).be('error');Because polling exits at the first match, a negated assertion that matches early fails quickly rather than waiting out the timeout.
Failure metadata
Failure.givenis theCallableblock (preserved as-is)Failure.expectedis the inner matcher'sexpected-valueFailure.messageincludes iteration count and elapsed time
Non-Callable actuals
expect(42).to.eventually.be(42) fails with expected a Callable for
eventually, but got 42 because the actual is not a Callable. The
block form is required.
Writing a custom matcher
Define a class that does Matcher and pass an instance to .be(...):
use BDD::Behave;
use BDD::Behave::Matcher;
class EvenMatcher does Matcher {
method matches($actual --> Bool) { ?($actual %% 2) }
method failure-message($actual --> Str) {
"expected $actual to be even";
}
method failure-message-negated($actual --> Str) {
"expected $actual not to be even";
}
method expected-value(--> Mu) { 'an even number' }
}
it 'is even', {
expect(4).to.be(EvenMatcher.new); # passes
expect(5).to.not.be(EvenMatcher.new); # passes
}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.
For the DSL-form alternative (defining matchers as a bundle of callbacks without writing a class), see Custom Matchers.
Composing matchers
Every type that does Matcher automatically gets .and and .or methods
that build AndMatcher / OrMatcher composites. They short-circuit, flatten
across chained calls, and report which inner matcher decided the outcome. See
Composable Matchers for details.