test.mock
import test.mock
testlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtest.mock.*needsimport test.mock.
Test doubles: functions that record how they were called, and stand-ins that replace a real one for the length of a test.
A recording function
import test { * }
describe('retry', @{
it('calls the operation once per attempt', @{
var operation = mock()
operation.raises_once(Error('first attempt'))
operation.returns('ok')
expect(retry(operation.fn, 3)).to_be('ok')
expect(operation).to_have_been_called_times(2)
})
})
mock() hands back a Mock, and mock.fn is the plain function to
pass wherever a callback is wanted. Matchers accept either, so
expect(operation) and expect(operation.fn) mean the same thing.
Replacing something that already exists
spy_on() swaps a function out in place and gives back a Mock that
both records and stands in for it:
var clock = { now: @{ return real_time() } }
var now = spy_on(clock, 'now')
now.returns(1000)
It works on a dictionary entry and on an instance property that holds a function. It does not work on a class method or a module function: Zuri classes are immutable once declared, and a module’s members cannot be assigned from outside it. Code that wants to be substitutable takes its collaborators as arguments or holds them in properties, which is the shape worth designing for anyway.
Lifetime
The runner calls restore_all() after every test, so a spy never
outlives the test that installed it, even one that failed partway
through. Nothing needs to be undone by hand.
Functions
mock()
test.mock(implementation: ?function, name: ?string) -> Mock
A new recording function.
var send = mock()
notify(send.fn)
expect(send).to_have_been_called_with('hello')
Parameters
implementation(?function) — what it does when called. Without one it records the call and returnsnil.name(?string) — used in failure messages;'mock'by default.
Returns Mock
spy_on()
test.spy_on(target, key: string) -> Mock
Replaces target[key] with a recording stand-in that calls through to
the original, and returns the Mock that now sits there.
Calling through is the default on purpose: a spy that changes behaviour
as well as observing it is a different tool, and saying so is one
.returns() away.
var gateway = { charge: @(amount) { return real_charge(amount) } }
var charge = spy_on(gateway, 'charge')
checkout(gateway)
expect(charge).to_have_been_called_times(1)
Parameters
target(dict|instance) — a dictionary, or an instance with a property holding a function.key(string)
Returns Mock
Raises TestSetupError when target is not a dictionary or an
instance, when it has no such entry, or when that entry does not hold
something callable. A class method cannot be spied on: classes are
immutable once declared.
mock_of()
test.mock.mock_of(value)
The Mock behind value, which may be a Mock already or the plain
function one handed out.
Parameters
value(any)
Returns — Mock: nil when value is neither.
restore_all()
test.mock.restore_all()
Undoes every spy and forgets every mock built so far.
The runner calls this after each test, so a spy installed by a test that failed halfway through still comes back out.
reset_all()
test.mock.reset_all()
Forgets the calls recorded by every live mock, leaving the mocks themselves and their behaviours in place.
Classes
Call
class test.mock.Call
One recorded call.
Fields
| Field | Type | Description |
|---|---|---|
args | The arguments, in order. | |
result | What the call returned, or nil when it raised. | |
error | The error it raised, or nil when it returned. | |
threw | Whether the call raised rather than returned. |
Constructor
test.mock.Call(args)
Mock
class test.Mock
A recording stand-in for a function.
Built by mock() and by spy_on(); there is no reason to construct one
directly.
Fields
| Field | Type | Description |
|---|---|---|
name | What the mock is called in failure messages. | |
fn | The plain function to pass around. | |
calls | Every call so far, oldest first. |
Constructor
test.Mock(name, implementation, replacing)
Parameters
name(?string)implementation(?function) — what the mock does when called. A mock with none returnsnil.replacing(?dict) — for a spy,{ target, key, original }naming what this mock was installed over, sorestore()knows what to put back.
Mock.returns()
test.Mock.returns(value)
Makes every call from now on return value.
Parameters
value(any)
Returns — Mock: itself, so calls chain.
Mock.returns_once()
test.Mock.returns_once(value) -> Mock
Makes the next call return value, once. Queue several to script a
sequence; once the queue runs dry the standing behaviour takes over
again.
Parameters
value(any)
Returns Mock
Mock.raises()
test.Mock.raises(value) -> Mock
Makes every call from now on raise value.
Parameters
value(Error)
Returns Mock
Mock.raises_once()
test.Mock.raises_once(value) -> Mock
Makes the next call raise value, once.
Parameters
value(Error)
Returns Mock
Mock.implements()
test.Mock.implements(implementation: function) -> Mock
Makes every call from now on run implementation and return what it
returns. The implementation receives exactly the arguments the mock was
called with.
Parameters
implementation(function)
Returns Mock
Mock.implements_once()
test.Mock.implements_once(implementation: function) -> Mock
Makes the next call run implementation, once.
Parameters
implementation(function)
Returns Mock
Mock.clear_behaviour()
test.Mock.clear_behaviour() -> Mock
Restores the original behaviour, dropping the standing one and anything still queued. Recorded calls are kept.
Returns Mock
Mock.call_count()
test.Mock.call_count() -> int
How many times the mock has been called.
Returns int
Mock.called()
test.Mock.called() -> bool
Whether the mock has been called at all.
Returns bool
Mock.call_args()
test.Mock.call_args() -> list
The arguments of every call, as a list of lists.
Returns list
Mock.last_call()
test.Mock.last_call() -> Call
The most recent call, or nil when there has not been one.
Returns Call
Mock.nth_call()
test.Mock.nth_call(index: int) -> Call
The index-th call, counting from zero, or nil when there have not
been that many. A negative index counts back from the most recent.
Parameters
index(int)
Returns Call
Mock.reset()
test.Mock.reset() -> Mock
Forgets every recorded call. The behaviour is left alone.
Returns Mock
Mock.restore()
test.Mock.restore() -> Mock
Puts back whatever this mock replaced, if it replaced anything.
Safe to call more than once, and on a mock that was never installed over something.
Returns Mock