#double(), ad-hoc test doubles

The simplest form takes a name (cosmetic, for error messages) and a list of method => value pairs. Unstubbed methods return Any and record the call, so doubles are permissive by design.

describe 'order summary', {
  it 'reads name and total from a user double', {
    my $user = double('User', name => 'alice', total => 99);

    expect($user.name).to.be('alice');
    expect($user.total).to.be(99);
  }
}

If the stub value is a Callable, the double invokes it with the call's arguments:

my $upper = double('Upper', shout => -> $s { $s.uc });

expect($upper.shout('hi')).to.be('HI');

#verifying class-based doubles

Pass a class as the first argument and the double becomes verifying: every stubbed method must exist on the class, and dispatching a method the class doesn't have dies. This catches typos and stubs that drift out of sync with the real implementation.

class Greeter {
  method hello($name) { "hello, $name" }
}

it 'stubs only methods that exist on the class', {
  my $g = double(Greeter, hello => 'mocked');

  expect($g.hello('world')).to.be('mocked');
  expect($g.double-class === Greeter).to.be(True);
}

# stubbing a method the class doesn't define dies at creation:
#   double(Greeter, missing => 1);   # โ†’ dies
# calling a method the class doesn't define dies at dispatch:
#   $g.bogus;                        # โ†’ dies

#allow().to.receive(), stub real objects

allow($target).to.receive('method') installs a temporary stub on a method of an existing class or instance. The stub is uninstalled when the example ends, restoring the original implementation.

it 'stubs hello on this one instance only', {
  my $g = Greeter.new;

  allow($g).to.receive('hello').and-return('STUB');

  expect($g.hello('alice')).to.be('STUB');                # stubbed
  expect(Greeter.new.hello('bob')).to.be('hello, bob');   # untouched
}

The full chain:

.and-return(v)Stub returns v. Omit the chain for Any.
.and-raise(ex)Stub throws the supplied exception.
.and-call-originalDelegate back to the real implementation.
.and-do(&cb)Invoke &cb with the call's positional args; its return is the result.

#partial mocks

allow($obj).to.receive('m') only replaces m. Every other method on the same instance keeps its real behavior, including methods that read or mutate state. Stub the collaborator boundaries, leave the rest of the object alone.

class Account {
  has Int $.balance is rw = 0;
  method deposit($n) { $!balance += $n; $!balance }
  method label       { "Account($!balance)" }
}

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 implementation
    expect($a.balance).to.be(150);          # real state mutation
  }
}

#allow-any-instance-of

Stubs an instance method at the class's method-table level, so every instance, existing or future, dispatches into the stub for the rest of the example.

class Repo { method find($id) { "real:$id" } }

it 'affects every instance for the duration of the example', {
  allow-any-instance-of(Repo).to.receive('find').and-return('stub');

  expect(Repo.new.find(1)).to.be('stub');
  expect(Repo.new.find(2)).to.be('stub');
}
Precedence. A per-instance allow($obj).to.receive('m') for the same method takes precedence over allow-any-instance-of(Class). Only that one instance sees the instance-specific stub.

#spy(), pass-through recorder

spy($real-instance) wraps each user-defined method on the instance with a recording stub that delegates to the real implementation. Returns the same instance. Use it when you want to verify a call without changing behavior.

describe 'spying on a real object', {
  it 'records calls without changing behavior', {
    my $g = Greeter.new;
    spy($g);

    expect($g.hello('alice')).to.be('hello, alice');   # real method ran
    expect($g).to.have-received('hello');              # call recorded
  }
}

#expect(obj).to.have-received()

Verify call records on a double, on a spy(...)-wrapped instance, or on any object whose method has been stubbed via allow(...). Chain count modifiers and argument matchers for precise expectations.

my $log = double('Logger');
$log.info('starting');
$log.info('done');

expect($log).to.have-received('info');                # at least one call
expect($log).to.have-received('info').twice;          # exactly two
expect($log).to.have-received('info').at-least(2);
expect($log).to.have-received('info').with('starting');
expect($log).to.have-received('info').with('starting').once;
ModifierMeaning
.once / .twice / .thriceExactly 1 / 2 / 3 calls.
.times(n) / .exactly(n)Exactly n calls.
.at-least(n)n or more calls.
.at-most(n)n or fewer calls.
.with(args, :named)Filter recorded calls. Combine with count modifiers.

#argument matchers

For each positional or named arg, supply a matcher object instead of a literal value.

MatcherMatches
anythingAny value, including undefined.
instance-of(Type)Anything for which $arg ~~ Type is true.
hash-including(:k<v>)Any Associative containing the listed key/value pairs.
array-including(item, ...)Any Positional containing all the listed items.
$log.info('hello', 42);
expect($log).to.have-received('info')
  .with(instance-of(Str), instance-of(Int));

$log.info({ user => 'alice', region => 'us', extra => 1 });
expect($log).to.have-received('info')
  .with(hash-including(user => 'alice'));

# matchers nest:
expect($log).to.have-received('info').with(
  hash-including(user => instance-of(Str), count => anything)
);
Auto-cleanup is per-example. A stub installed inside an it (or in before-each) is removed before the next example. A stub installed in before-all lives for the entire describe it sits in.