Behavior-driven
development for Raku.

A complete behavior-driven testing framework for Raku, with expressive spec syntax, rich matchers, first-class mocks, built-in coverage, and pluggable output formatters. Specs that read like prose and run like tests.

Run your suite with the included behave CLI, focus on a single example with tags, bisect flaky specs down to the offending interaction, and benchmark hot paths without leaving the framework. Tight feedback loops, readable failure output, and tests that double as living documentation for the rest of your team.

specs/account-spec.raku
// why behave

Specs that read like the conversation you had about the bug.

Group related expectations with describe and context. Lazy-evaluate fixtures with let. Bind the subject under test with subject. Behave's spec syntax maps the way you actually think about behavior to Raku without losing the language's syntactic personality.

  • Lazy & memoized lets. Values are computed once per example, reset between examples.
  • Composable matchers. Chain .and, .or, .not, with short-circuit semantics built in.
  • Auto-cleaned mocks. Every stub installed in an example is uninstalled when the example ends.
  • Junctions, ranges, regex. All the smart-match power Raku already gives you.
use BDD::Behave;

describe 'String#flip', {
  let(:word, { 'hello world' });

  it 'reverses the characters', {
    expect(:word.flip).to.be('dlrow olleh');
  }

  it 'is its own inverse', {
    expect(:word.flip.flip).to.be(:word);
  }

  context 'on the empty string', {
    let(:word, { '' });

    it 'returns the empty string', {
      expect(:word.flip).to.be('');
    }
  }
}
// matchers

Rich matchers, one consistent grammar.

Behave's matcher role is small and the built-ins are deep. Numeric tolerance, collection membership, attribute checks, regex, async streams, promises, exceptions, all expressed through the same expect(...).to.matcher(...) chain, with first-class .not and structural failure metadata.

  • Numeric. be-greater-than, be-between(1, 10).exclusive, be-within(0.1).of(5.0)
  • Collections. include, contain-exactly, start-with, all(Int)
  • Async. be-kept, complete-within(2), emit(1, 2, 3), eventually
  • Custom. Define matchers with define-matcher or compose existing ones.
expect(5.05).to.be-within(0.1).of(5.0);

expect([1, 2, 3]).to.contain-exactly(3, 1, 2);
expect([1, 2, 3]).to.all(Int);
expect('hello world').to.match(/world/);

expect({ die "boom" }).to.raise-error(X::AdHoc, /boom/);

my $counter = 0;
expect({ $counter += 5 }).to.change({ $counter })
  .from(0).to(5).by(5);

expect(start { compute() }).to.complete-within(1);

expect(:user.age).to.be-greater-than(17)
  .and.be-less-than(65);
// mocks & spies

Stand-ins that don't leak between examples.

double(SomeClass) produces a verifying double that rejects stubs for methods the class doesn't have. allow($obj).to.receive('m') stubs a single method on a real instance. Every other method keeps its real behavior, perfect for partial mocks. spy($real) records every call without changing what the object does. Behave uninstalls everything at end-of-example, so the next example sees the world as it was.

describe 'partial mock', {
  it 'stubs label but lets deposit run for real', {
    my $a = Account.new(balance => 100);

    allow($a).to.receive('label').and-return('STUB');

    expect($a.label).to.be('STUB');           # stubbed
    expect($a.deposit(50)).to.be(150);        # real method
    expect($a.balance).to.be(150);            # real state
  }

  it 'records calls on a spy', {
    my $log = double('Logger');
    $log.info('starting');
    $log.warn('careful', :reason);

    expect($log).to.have-received('info').once;
    expect($log).to.have-received('warn')
      .with(anything, :reason);
  }
}
// install

Thirty seconds to first green.

One command to install. One specs/ directory to create. behave finds and runs every *spec.raku it sees.

$ zef install BDD::Behave
Full install guide → Browse the docs →