You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lua-lemock/build/userguide.t2t

353 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 mockstub 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()
```