|
|
LeMock User Guide
|
|
|
v 0.6
|
|
|
2009-05-30
|
|
|
|
|
|
%!postproc(html): – –
|
|
|
%!postproc(tex): – --
|
|
|
|
|
|
= 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 #anyargs] 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 ==[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()
|
|
|
```
|