Rich matchers, one grammar.
Every assertion in Behave reads expect(actual).to.matcher(expected).
Negate any matcher with .not. Compose with .and / .or.
Plug in your own with define-matcher. They integrate with the same role
the built-ins do.
#core
be wraps Raku's smart-match operator (~~) so values,
regexes, ranges, and junctions all compose naturally. eq uses
order-dependent structural equality via eqv.
expect(42).to.be(42);
expect('hello').to.be(/hell/);
expect(5).to.be(1..10);
expect($x).to.be(any(1, 2, 3));
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 });
#collections
| include | Membership across arrays, hashes, sets, ranges, and substrings. |
| contain-exactly | Multiset equality. Counts and totals must match. |
| match-array | Array-form alias for contain-exactly. |
| start-with / end-with | In-order prefix / suffix for sequences, or each-prefix-AND for strings. |
| all | Every element must match the inner matcher (a value, type, range, regex, or matcher). |
expect([1, 2, 3]).to.include(1, 3);
expect({ a => 1, b => 2 }).to.include(:a(1));
expect('hello world').to.include('world');
expect(1..10).to.include(5);
expect([1, 1, 2]).to.contain-exactly(1, 2, 1); # multiset
expect([1, 2, 3]).to.start-with(1, 2);
expect('hello world').to.start-with('hello', 'h');
expect([1, 5, 10]).to.all(1..10);
expect(['foo', 'food']).to.all(/^foo/);
expect(@rows).to.all(IncludeMatcher.new(:expected([status => 'ok'])));
#types & attributes
be-a uses smart-match against a type, so subclasses, composed roles,
and subset types all match. be-an-instance-of is strict:
the runtime type must be exactly the given type. respond-to uses the
meta-object protocol's ^can. have-attributes checks several
accessors in one call.
expect(Dog.new).to.be-a(Animal); # subclass passes
expect(Dog.new).to.be-an-instance-of(Animal); # strict, fails
subset Positive of Int where * > 0;
expect(5).to.be-a(Positive); # passes
expect(Calculator.new).to.respond-to('add', 'subtract');
expect($alice).to.have-attributes(
:name,
:age(BeAMatcher.new(:type(Int))),
);
#numeric
Comparison matchers, inclusive / exclusive ranges, and tolerance checks.
All accept Real values, so Int, Rat, and
Num mix freely. Undefined or non-numeric actuals record a failure
rather than throwing.
expect(5).to.be-greater-than(3);
expect(5).to.be-gte(5); # alias
expect(3).to.be-lt(5);
expect(5).to.be-between(1, 10); # inclusive default
expect(5).to.be-between(1, 10).exclusive; # strict interior
expect(1).to.be-between(1, 10).exclusive; # fails
expect(5.05).to.be-within(0.1).of(5.0); # |actual - target| <= delta
expect(5).to.be-within(0).of(5); # equality with delta 0
#strings & regex
match smart-matches a Str actual against a Regex,
with full Raku regex syntax including modifiers and rx// forms.
Truthiness and nil checks round out the basics.
expect('abc123').to.match(/\d+/);
expect('HELLO').to.match(rx:i/hello/); # case-insensitive
expect('hello').to.match(/^hello$/); # anchored
expect(True).to.be-truthy;
expect('').to.not.be-truthy;
expect([]).to.not.be-truthy; # empty collections are falsy in Raku
expect(Nil).to.be-nil;
expect(Int).to.be-nil; # type objects are undefined
expect(0).to.not.be-nil; # defined value
#exceptions
Wrap the code under test in a block. Optionally filter by exception type and message, including subclass / role-composition matches. Failures surface what actually happened so you can diagnose without re-running.
expect({ die "boom" }).to.raise-error;
expect({ X::Demo.new.throw }).to.raise-error(X::Demo); # exact / subclass
expect({ die "code=42" }).to.raise-error(/'code=42'/);
expect({ die "boom" }).to.raise-error.with-message('boom');
# A different type raised? The failure message tells you which:
# expected block to raise X::Demo, but raised X::AdHoc: boom
#change
Observe state transitions. change takes an action block and an
observable block. It invokes the observable, runs the action, invokes the
observable again, and compares with eqv. Chain .from,
.to, .by, .by-at-least, and .by-at-most
for precise transitions.
my $counter = 0;
expect({ $counter++ }).to.change({ $counter });
expect({ $counter = 10 }).to.change({ $counter })
.from(1).to(10);
expect({ $score += 5 }).to.change({ $score })
.by-at-least(1).by-at-most(10);
# Negation with constraints: change must NOT be this specific transition
expect({ $counter = 7 }).to.not.change({ $counter }).from(0).to(10);
#async & streams
First-class matchers for Promise, Supply, and
Channel. Configurable timeouts. eventually polls a block
until an inner matcher passes. Perfect for eventually-consistent state.
expect(Promise.kept('done')).to.be-kept;
expect(start { compute() }).to.be-kept(0.5); # 500ms timeout
expect(Promise.broken('oops')).to.be-broken;
expect($promise).to.complete-within(1);
expect(Supply.from-list(1, 2, 3)).to.emit(1, 2, 3);
expect($supply).to.emit-at-least(3, :within(0.5));
expect($supply).to.complete(:within(1));
# Poll until match, with timeout + interval
expect({ get-job-status() }).to.eventually.be('done');
expect({ counter() }).to.eventually(:timeout(5)).be-greater-than(100);
#compose with .and / .or
Every type that does Matcher automatically gets .and /
.or methods. Composites short-circuit, flatten across chained calls,
and report which inner matcher decided the outcome.
expect(:user.age).to.be-greater-than(17)
.and.be-less-than(65);
expect(:status).to.be('queued').or.be('running');
# Negation flips the boolean result *before* the framework decides whether
# to record a failure. Every matcher honors .not the same way.
expect([1, 2, 3]).to.not.include(99);
#writing your own matcher
Define a class that does the Matcher role, or use the
define-matcher helper for a class-free declaration. Custom matchers integrate
with .not, .and, and the failure reporter exactly like
built-ins.
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' }
}
describe 'EvenMatcher', {
it 'matches even numbers', {
expect(4).to.be(EvenMatcher.new);
expect(5).to.not.be(EvenMatcher.new);
}
}
Failure.given, Failure.expected, and a structured
message, so alternate formatters (JSON, JUnit, HTML) render them
identically to the built-ins.