mocks

Doubles & Stubbing

Behave gives you two complementary ways to stand in for collaborators:

  • double(...) creates a new stand-in object from scratch, useful when there's no real implementation to lean on.

  • allow(obj).to.receive('method') stubs a method on an existing object or class, so the real type stays in play but specific calls are intercepted.

Both are exported from BDD::Behave. Stubs installed via allow(...) are automatically uninstalled at the end of each example, so they never leak between specs.

Creating an ad-hoc double

The simplest form takes a name and a list of method => value pairs:

use BDD::Behave;

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);
  }
}

The name is purely cosmetic: it shows up in error messages from the double itself.

double() with no arguments produces an anonymous double:

my $d = double;
$d.poke;
expect($d.received('poke')).to.be(True);

What unstubbed methods do

Calling a method on an ad-hoc double that you didn't stub returns Any. The call is still recorded, so you can verify it happened:

my $log = double('Logger');
$log.warn('careful');                    # returns Any
expect($log.received('warn')).to.be(True);

Ad-hoc doubles are permissive by design: calls you haven't stubbed return Any instead of erroring.

Callable stubs

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

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

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

This is useful when the return value should depend on the input.

Adding stubs after creation

add-stub accepts the same method => value pairs as double() and returns the double, so you can chain:

my $cfg = double('Config').add-stub(theme => 'dark', font => 'mono');

expect($cfg.theme).to.be('dark');
expect($cfg.font).to.be('mono');

Verifying calls

Every call is captured. The double exposes a small set of methods for asking what happened:

MethodReturns
received($method)True if $method was called at least once.
call-count($method)Number of times $method was called.
calls-of($method)List of Call objects for $method, in the order they happened.
callsList of every Call made on the double.
resetClears the recorded calls (stubs survive).

A Call exposes method, args (positional), named, file, and line.

my $log = double('Logger');
$log.info('starting');
$log.warn('careful', :reason<low-disk>);

expect($log.call-count('info')).to.be(1);
expect($log.calls-of('warn')[0].named<reason>).to.be('low-disk');

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" }
}

describe 'class-based double', {
  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);
    expect($g.double-name).to.be('Greeter');
  }
}

What you can't do with a class-based double:

  • Stub a method the class doesn't define: double(Greeter, missing => 1) dies at creation.

  • Call a method the class doesn't define: $g.bogus dies at dispatch.

The double does not invoke the real implementation. The class is only consulted for validation.

Reserved method names

Because Double exposes the verification API on the same object you call stubbed methods on, a handful of method names are reserved and cannot be used as stub keys:

add-stub, call-count, calls, calls-of, double-class, double-name, received, reset, stubs.

If your real collaborator uses one of these names, stub the method directly on a real instance with allow(obj).to.receive(...) instead.

Stubbing real objects with allow

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

The target can be:

  • An instance: only that one instance gets the stub. Sibling instances dispatch normally.

  • A class (type object): affects dispatch through the class's method table, so every instance of the class sees the stub. Use allow-any-instance-of(Class) for the same effect with clearer intent.

  • A Double: the stub is wired through the double's stub table.

Partial mocking

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. This is the core of partial mocking: 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
  }
}

You can stub multiple methods on the same instance independently. Each stub is tracked separately and removed at the end of the example.

Return values

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

describe 'allow + and-return', {
  it 'stubs hello on this one instance', {
    my $g = Greeter.new;
    allow($g).to.receive('hello').and-return('STUB');

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

.and-return is optional: calling allow($g).to.receive('hello') with no follow-up stubs the method to return Any.

Raising exceptions

.and-raise makes the stubbed method throw the supplied exception:

allow($repo).to.receive('find').and-raise(X::AdHoc.new(payload => 'not found'));

my $msg = '';
try { $repo.find(7); CATCH { default { $msg = .message } } }
expect($msg).to.be('not found');

Delegating to the real implementation

.and-call-original makes the stubbed method delegate back to the original. The most common use is "re-stubbing" a method back to its real behavior after a previous allow set up a return value:

allow($g).to.receive('hello').and-return('mocked');
expect($g.hello('a')).to.be('mocked');

allow($g).to.receive('hello').and-call-original;
expect($g.hello('a')).to.be('hello, a');

.and-call-original is not supported on a Double: there is no original implementation to call back to.

Dynamic stubs

.and-do(&callable) invokes the supplied callable with the call's positional arguments. The callable's return value becomes the stubbed method's return value:

allow($g).to.receive('hello').and-do(-> $name { "STUB($name.uc())" });

expect($g.hello('alice')).to.be('STUB(ALICE)');

Replacement semantics

A second allow($t).to.receive('m') on the same target+method pair replaces the first stub. Only one allow-stub per (target, method) is active at a time:

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

expect($g.hello('a')).to.be('second');

Stubbing across every instance: allow-any-instance-of

allow-any-instance-of(Class).to.receive('m') 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" }
}

describe 'stub on every instance', {
  it 'affects every instance of Repo 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');
  }
}

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, and other instances continue to see the class-wide one.

allow-any-instance-of requires a type object: passing an instance dies with a clear error. The same method-existence validation as allow(...) applies: stubbing a method the class doesn't define dies before any wrap is installed. The full .and-return / .and-raise / .and-call-original / .and-do chain works the same way as on allow(...).

Stubs in before-all vs before-each

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:

describe 'group-wide stub', {
  before-all {
    allow(Repo).to.receive('find').and-return('group-stub');
  }

  it 'first example sees it', { expect(Repo.find(1)).to.be('group-stub') }
  it 'second example also sees it', { expect(Repo.find(2)).to.be('group-stub') }
}

Verifying stubs

allow($t).to.receive('m') rejects method names that don't exist on the target's class. This catches typos and stubs that drift out of sync with the real implementation:

allow($g).to.receive('imaginary');   # dies: no such method on Greeter
allow(Greeter).to.receive('also-no'); # dies: same check on the class

For a class-based Double, the stubbed method must exist on the wrapped class.

Spies and have-received

Doubles record their calls automatically. To record calls on a real object, install an allow(...).to.receive(...) stub or use spy(...). Then verify with expect(obj).to.have-received('method').

spy(...): pass-through recorder

spy() has three forms:

FormResult
spy() / spy('Name')Same as double() / double('Name'). Permissive recording double.
spy(SomeClass)Same as double(SomeClass). Class-based verifying double.
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.
class Greeter { method hello($name) { "hello, $name" } }

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');
    expect($g).to.have-received('hello');
  }
}

A spy on a real instance only affects that one instance. Sibling instances dispatch normally. Stubs installed by spy(...) are auto-cleaned at end of example, like allow(...) stubs.

expect(obj).to.have-received('method')

have-received checks call records on a Double, on a spy(...)-wrapped instance, or on any object whose method has been stubbed via allow(...).

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

expect($log).to.have-received('info');     # at least one call

For real objects, you must install a recorder first (via allow(...) or spy(...)):

my $repo = Repo.new;
allow($repo).to.receive('find').and-call-original;

$repo.find(7);
expect($repo).to.have-received('find');

If no stub is installed when verifying a real object, have-received records a clear failure asking you to set one up.

expect(obj).not.to.have-received('m') asserts the method was not called.

Call-count modifiers

ModifierMeaning
.onceexactly 1 call
.twiceexactly 2 calls
.thriceexactly 3 calls
.times(n)exactly N calls
.exactly(n)alias for .times(n)
.at-least(n)N or more calls
.at-most(n)N or fewer calls
expect($log).to.have-received('info').twice;
expect($log).to.have-received('info').at-least(2);
expect($log).to.have-received('info').at-most(3);

Each chained modifier replaces any failure recorded by the previous step, so chains like .have-received('m').with('a').twice produce at most one failure even if the first check would have failed on its own.

Argument matching: .with(...)

.with(args) filters the recorded calls and only counts those whose args match.

$log.info('starting');
$log.info('done');

expect($log).to.have-received('info').with('starting');     # passes
expect($log).to.have-received('info').with('starting').once; # passes

Both positional and named args are supported:

allow($g).to.receive('greet').and-return('hi');
$g.greet(:lang<en>);
expect($g).to.have-received('greet').with(:lang<en>);

Argument matchers

For each positional or named arg, you can 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'));

$log.info([1, 2, 3, 4, 5]);
expect($log).to.have-received('info').with(array-including(2, 4));

Matchers nest inside hash-including and array-including:

expect($log).to.have-received('info').with(
  hash-including(user => instance-of(Str), count => anything)
);

Limits

  • spy($real-instance) wraps user-defined methods. Methods inherited from Mu/Any and submethods (e.g. BUILD, POPULATE) are not wrapped.

BDD::Behave v0.9.4

Behavior driven development framework

Authors

  • Greg Donald

License

Artistic-2.0

Dependencies

Provides

  • BDD::Behave
  • BDD::Behave::Benchmark
  • BDD::Behave::Benchmark::Baseline
  • BDD::Behave::Benchmark::Format
  • BDD::Behave::Bisect
  • BDD::Behave::Colors
  • BDD::Behave::Configuration
  • BDD::Behave::Coverage
  • BDD::Behave::DSL
  • BDD::Behave::Diff
  • BDD::Behave::DocExtractor
  • BDD::Behave::DryRun
  • BDD::Behave::Expectation
  • BDD::Behave::Failure
  • BDD::Behave::FailureStore
  • BDD::Behave::Failures
  • BDD::Behave::Files
  • BDD::Behave::Formatter
  • BDD::Behave::Formatter::Documentation
  • BDD::Behave::Formatter::HTML
  • BDD::Behave::Formatter::JSON
  • BDD::Behave::Formatter::JUnit
  • BDD::Behave::Formatter::JsonEvents
  • BDD::Behave::Formatter::Progress
  • BDD::Behave::Formatter::Registry
  • BDD::Behave::Formatter::TAP
  • BDD::Behave::Formatter::Tree
  • BDD::Behave::LetRuntime
  • BDD::Behave::Matcher
  • BDD::Behave::Matcher::Async
  • BDD::Behave::Matcher::Boolean
  • BDD::Behave::Matcher::Change
  • BDD::Behave::Matcher::Collection
  • BDD::Behave::Matcher::Core
  • BDD::Behave::Matcher::Custom
  • BDD::Behave::Matcher::Exception
  • BDD::Behave::Matcher::Numeric
  • BDD::Behave::Matcher::String
  • BDD::Behave::Matcher::Type
  • BDD::Behave::Mock::Allow
  • BDD::Behave::Mock::ArgMatcher
  • BDD::Behave::Mock::Double
  • BDD::Behave::Mock::HaveReceived
  • BDD::Behave::Mock::Spy
  • BDD::Behave::Mock::Stub
  • BDD::Behave::Parallel
  • BDD::Behave::Parallel::Distribution
  • BDD::Behave::Parallel::EventStream
  • BDD::Behave::Parallel::Manifest
  • BDD::Behave::Parallel::Queue
  • BDD::Behave::Parallel::WorkerPool
  • BDD::Behave::Runner
  • BDD::Behave::SharedContexts
  • BDD::Behave::SharedExamples
  • BDD::Behave::Slang
  • BDD::Behave::SpecLoader
  • BDD::Behave::SpecRegistry
  • BDD::Behave::SpecTree
  • BDD::Behave::SpecTree::Core
  • BDD::Behave::SpecTree::Example
  • BDD::Behave::SpecTree::ExampleGroup
  • BDD::Behave::SpecTree::Suite
  • BDD::Behave::Time
  • BDD::Behave::TypeName
  • BDD::Behave::Version
  • BDD::Behave::Watch
  • BDD::Behave::Watch::Session
  • BDD::Behave::Watch::SmartSelector
  • BDD::Behave::Watch::UI
  • BDD::Behave::Watch::Watcher
  • BDD::Behave::Worker

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.