Introduction

Mock objects replace difficult external objects during unit testing by simulating the behaviors of the replaced objects. This is done by first recording actions and their responses with the mock objects, and then switching to replay mode. During replay mode the mock objects simulate the replaced objects by looking up actions and replaying the recorded responses, and finally verifying that all expected actions where completely replayed.

Actions are stored in a list in a special controller object. During replay the list is searched in recording order for the first matching action that can be replayed.

Restrictions on the actions can be inserted during the recording phase. An action can have a maximum count of how many times it will be replayed, and a minimum count of how many times it must be replayed to be satisfied. An action can depend on any set of other actions, and can not be replayed before all of its depended actions are satisfied. An action can close any set of actions when it is replayed, which stops all further replaying of the closed actions. This is good for simulating state changes.

Example

This example tests that the insert_data function of the foo module handles a missing data base table gracefully.

-- Setup
require 'lemock'
local mc = lemock.controller()
local sqlite3 = mc:mock()
local env     = mc:mock()
local con     = mc:mock()
package.loaded.luasql = nil
package.preload['luasql.sqlite3'] = function ()
    luasql = {}
    luasql.sqlite3 = sqlite3
    return sqlite3
end

-- Record
sqlite3()                 ;mc :returns(env)
env:connect('/data/base') ;mc :returns(con)
con:execute(mc.ANYARGS)   ;mc :error('LuaSQL: no such table')
con:close()
env:close()

-- Replay
mc:replay()
require 'foo'
local res = foo.insert_data(17)
assert(res==false)

--Verify
mc:verify()

First a controller is created. Then three mock objects are created, one for the sqlite3 module, and two for objects returned by the (simulated) module.

Then a preloader for the sqlite3 module is installed, which returns the sqlite3 mock object instead of the actual sqlite3 module.

In the record phase the expected calls and their return values (or thrown errors) are recorded. The order is not significant, so this simplified test will not detect if the close method is called before the execute method.

In the replay phase the tested module is loaded and executed. It will use the mock objects instead of the real data base, and if it makes any unrecorded calls, an error is thrown.

The verify phase asserts that all recorded actions have been replayed. If the foo module for example forgets to call the close method, verify throws an error.

The Mock Object

Mock objects are empty objects with special Lua meta methods that detect actions performed with the object. What happens depends on the state (recording or replaying) of the controller which created the mock object. During recording the mock object adds the action to the controller's list of recorded actions. During replay the mock object looks for a matching recorded action that can be replayed, and simulates the action.

Some action attributes can not be inferred by the mock objects, for example return values. These attributes have to be added afterwards with special controller methods, and always affect the last recorded action.

Actions

Mock objects detect four types of actions: assignment, indexing, method call, and self call. During replay an action will only match if it is the very same action, that is, the same type of action performed on the same mock object with all the same arguments. There are however special arguments that can be used during recording.

require 'lemock'
local mc = lemock.controller()
local m = mc:mock()

m.x = 17    -- assignment
r = m.x     -- indexing
m.x(1,2,3)  -- method call
m:x(1,2,3)  -- method call
m(1,2,3)    -- self call

Anyargs

An anyarg is a special argument used when recording, that will match any argument during replay. It can appear anywhere and any times in an argument list, or as the argument in an assignment, to replace real arguments. There is also anyargs, which will match any number (including zero) of any arguments. Anyargs can only appear as the last argument of an argument list. Anyarg and anyargs are handy when the actual values of the arguments during replay are unimportant or unknown.

Anyarg and anyargs are constants defined in the controller object.

Example

This example tests that the fetch_data function of module foo waits a while and retries when no data is immediately available, and that it updates the value of lasttime.

require 'lemock'
local mc = lemock.controller()
local con = mc:mock()

con:poll()           ;mc :returns(nil)
con:sleep(mc.ANYARG)
con:poll()           ;mc :returns('123.45')
con.lasttime = mc.ANYARG

mc:replay()
require 'foo'
local res = foo.fetch_data(con)
assert( math.abs(res-123.45) < 0.0005 )

mc:verify()

The Controller

The controller's main purpose is to store the recorded actions, create mock objects, switch to replay mode, and verify the completion of the replay phase. But it is also needed to set or change special action attributes during recording.

It is possible, although doubtfully useful, to use several controllers in parallel during a single unit test. Each controller maintains its own action list and state, and mock objects remember which controller they belong to.

Returns & Error

The by far most useful special action attribute is the return value. Indexing actions can return a single value, while call actions and self call actions can return a list of values. The return value is set with the returns method, and it is an error to set the return value twice for the same action.

For purposes of unit testing it is often useful to simulate errors. All actions can raise an error, and return an error value (usually a string). The return value is set with the error method. An action can not have both a return value and raise an error.

Example

require 'lemock'
local mc = lemock.controller()
local m = mc:mock()

m:foo(17)  ;mc :returns(nil, "index out of range")
m:bar(-1)  ;mc :error("invalid index")

Label & Depend

Dependencies block actions from replaying until other actions have replayed first. They can be used to verify that actions are being replayed in a valid order.

To add dependencies, actions must first be labeled with one or more labels. The same label can be given to several actions. As long as some action with the label remains unsatisfied, that label is blocked, and all actions depending on that label will not replay.

Example

This (contrived) example tests that function draw_square in module foo calls all the necessary drawing methods of a square object in a correct order. Note that there can be more than one correct order.

require 'lemock'
local mc = lemock.controller()
local square = mc:mock()

square:topleft()   ;mc :label('tl')
square:topright()  ;mc :label('tr')
square:botleft()   ;mc :label('bl')
square:botright()  ;mc :label('br')
square:leftedge()  ;mc :label('edge') :depend('tl', 'bl')
square:rightedge() ;mc :label('edge') :depend('tr', 'br')
square:topedge()   ;mc :label('edge') :depend('tl', 'tr')
square:botedge()   ;mc :label('edge') :depend('bl', 'br')
square:fill()      ;mc                :depend('edge')

mc:replay()
require 'foo'
foo.draw_square( square )

mc:verify()

This example demonstrates two different ways of using dependencies. All the corners have unique labels, because each edge depend on a set of specific corners. But all the edges have the same label, because the fill operation only depends on all edges have been satisfied.

Times

The default for a recorded action is to be replayed exactly once. times(2) changes that to exactly two times, and times(1,2) changes it to at least one time and at most two times.

When the action has been replayed the least count times it is satisfied, which means verify will not complain about it, and it no longer blocks actions that depend on this action from being replayed. If the least count is zero the action is automatically satisfied and need not be replayed at all, i.e., it is optional.

When the action has been replayed the most count times it will not replay any more. The most replay count can be set to infinity (math.huge or 1/0), in which case the action will never stop replaying.

anytimes() can be used as an alias for times(0,1/0), and atleastonce() can be used as an alias for times(1,1/0).

Example

This example tests that method update is called at least once.

require 'lemock'
local mc = lemock.controller()
local con = mc:mock()

con:log(mc.ANYARGS) ;mc                :anytimes()
con:update('x',3)   ;mc :returns(true) :atleastonce()

mc:replay()
require 'foo'
local watcher = foo.mk_watcher( con )
watcher:set( 'x', 3 )

mc:verify()

Close

Close can be used to simulate state changes in a limited way. When an action with a close statement is replayed for the first time, it will permanently block all labels in its close statement, so that actions with these labels no longer replays. This passes on matching to later actions in the action list, which may for example have different return values.

The closing simply blocks the labels, and it has nothing to do with max replay counts or if closed actions have been satisfied or not. Closing an unsatisfied action however results in an immediate failure.

Example

This example tests that the dump function of module foo calls the myio functions in a correct order. The read function can be called any number of times, until it is closed by the close function.

require 'lemock'
local mc = lemock.controller()
local myio = mc:mock()
local fs   = mc:mock()

myio.open('abc', 'r') ;mc :returns(fs)
mc :label('open')

fs:read(mc.ANYARG) ;mc :returns('data')
mc :atleastonce() :label('read') :depend('open')

fs:close() ;mc :returns(true)
mc :depend('open') :close('read')

mc:replay()
require 'foo'
foo.dump(myio, 'abc', 128)

mc:verify()

Tricks

Mock objects are completely empty, and do not contain any methods or properties of their own. If they did, that would risk shadowing a name of a simulated object's method or property. There is however nothing preventing users from defining methods and properties in mock objects. This way mock objects can be turned into stubs, or a kind of mockÔÇôstub hybrid.

Method Overloading

Lua does not support method overloading, but it can be (and sometimes is) implemented manually by testing of function arguments. This presents a problem to LeMock, because it matches exact arguments, and anyargs in not sufficient. In this case the mock object can be extended with a dispatcher function.

Example

This example shows a mock object with an overloaded add function. The stub function can not be defined in the usual way, because that would record an assignment action; it needs to be defined with rawset.

require 'lemock'
local mc = lemock.controller()
local m = mc:mock()

do
local function add (a, b)
    if type(a) == 'number' then
        return m.add_number(a, b)
    else
        return m.add_string(a, b)
    end
end
rawset( m, 'add', add ) -- not recorded
end -- do

m.add_number(1, 2)         ;mc :returns(3)
m.add_string('foo', 'bar') ;mc :returns('foobar')

mc:replay()
assert_equal( 3, m.add(1, 2) )
assert_equal( 'foobar', m.add('foo', 'bar') )

mc:verify()

2009-05-31