#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

includeMembership across arrays, hashes, sets, ranges, and substrings.
contain-exactlyMultiset equality. Counts and totals must match.
match-arrayArray-form alias for contain-exactly.
start-with / end-withIn-order prefix / suffix for sequences, or each-prefix-AND for strings.
allEvery 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);
  }
}
Tooling-ready failures. Custom matchers populate Failure.given, Failure.expected, and a structured message, so alternate formatters (JSON, JUnit, HTML) render them identically to the built-ins.