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/src/main.nw

762 lines
24 KiB

Lua Easy Mock -- LeMock
Copyright (C) 2009 Tommy Pettersson <ptp@lysator.liu.se>
See terms in file COPYRIGHT, or at http://lemock.luaforge.net
@
Introduction
############
<<Userguide 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.
@
Manage Actions
##############
The Action List
===============
Actions are stored in a list in a special Controller object. To keep it
simple, the list is a simple array table. The controller has an
[[add_action]] method, and a [[lookup]] method. Actions are added at the
end, and looked up from the beginning.
add_action
----------
<<Unit test for class Controller method add_action>>=
function add_action_at_the_end_test ()
mc:add_action( 7 )
mc:add_action( mc )
assert_equal( 7, mc.actionlist[1] )
assert_equal( mc, mc.actionlist[2] )
end
<<Class Controller method add_action>>=
function Controller:add_action (a)
assert( a ~= nil, "lemock internal error" ) -- breaks array property
table.insert( self.actionlist, a )
end
@
lookup and actions
------------------
It should be easy to add new action types, so the [[lookup]] method
delegates matching to the [[match]] method of the Action objects in the
list. If there is no match an error is thrown with a list of all actions
that could have been matched.
<<Unit test for class Controller method lookup>>=
function lookup_returns_first_matching_action_test ()
local Fake_action
<<Simple Fake_action class>>
local a1 = Fake_action:new(1)
local a2 = Fake_action:new(2)
local a3 = Fake_action:new(1)
local ok, err = pcall( function() mc:lookup( a1 ) end )
assert_false( ok, "match in empty list" )
assert_match( "Unexpected action <faked action>", err )
mc:add_action( a1 ) mc:add_action( a2 ) mc:add_action( a3 )
local ok, err = pcall( function() mc:lookup( a1 ) end )
assert_false( ok, "should not match any action" )
assert_match( "Unexpected action <faked action>", err )
assert_equal( a1, mc:lookup( a2 ), "did not find first match" )
end
<<Class Controller method lookup>>=
function Controller:lookup (actual)
for action in self:actions() do
if action:match( actual ) then
return action
end
end
<<Find all expected actions>>
error( sfmt( "Unexpected action %s, expected:\n%s\n"
, actual:tostring()
, table.concat(expected,'\n')
)
, 0
)
end
@
There is no point in listing index actions for callables, since they will
just mirror the call actions.
Sorting the list will make it easier for the user to conclude that an
expected action is "missing", because there will be a definite place in the
list where the action would have been.
<<Find all expected actions>>=
local expected = {}
for _, a in ipairs( self.actionlist ) do
if a:is_expected() and not a.is_callable then
expected[#expected+1] = a:tostring()
end
end
table.sort( expected )
if #expected == 0 then
expected[1] = "(Nothing)"
end
@
Many functions iterate over all the actions in the action list.
<<Unit test for class Controller method actions>>=
function actions_dont_iterate_empty_list_test ()
for a in mc:actions() do
fail( "iterates on empty list" )
end
end
function actions_iterate_over_entire_list_exactly_once_test ()
local l = { {},{},{} }
for _, a in ipairs( l ) do
mc:add_action( a )
end
for a in mc:actions() do
assert_nil( a.check )
a.check = true
end
for _, a in ipairs( l ) do
assert_true( a.check )
end
end
<<Class Controller method actions>>=
function Controller:actions (q)
local l = self.actionlist
local i = 0
return function ()
i = i + 1
return l[i]
end
end
@
The Controller object will allow the user to set return values or
restrictions on the last recorded action.
last
----
<<Unit test for class Controller method get_last_action>>=
function get_last_action_returns_last_element_test ()
local l = { 'a', 'foo', 17 }
for i = 1, #l do
mc:add_action( l[i] )
local res = mc:get_last_action()
assert_equal( l[i], res )
end
end
function get_last_action_fails_on_empty_list_test ()
local ok, err = pcall( function() mc:get_last_action() end )
assert_false( ok, "Found last action in empty list" )
assert_match( "No action is recorded yet", err )
end
<<Class Controller method get_last_action>>=
function Controller:get_last_action ()
local l = self.actionlist
if #l == 0 then
error( "No action is recorded yet.", 0 )
end
return l[#l]
end
@
Actions
=======
Each Action type is implemented as a specialized class so it is easy to add
new action types. The action class is responsible for storing information
and state. The Mock class and Callable class are responsible for catching
and recording/replaying the action with help from the information in the
Action object.
Action types differ in what information they need to store, and therefore
how they are constructed, matched by lookup, and printed as strings. The
concrete or extended implementations of the methods [[new]], [[match]], and
[[tostring]], are therefore implemented together with the corresponding
Mock and Callable meta methods responsible for catching the action,
arranged in separate source files in the action/ directory.
New
---
New Action objects are created with default [[min_replays]] and
[[max_replays]] equal to one, which means each recorded action is expected
to be replayed exactly once. This method will be extended by the the
concrete Action class types.
<<Unit test for class Action.generic method new>>=
function new_action_has_right_default_values_test ()
assert_equal( 0, a.replay_count )
assert_equal( 1, a.min_replays )
assert_equal( 1, a.max_replays )
end
<<Class Action.generic method new>>=
function Action.generic:new (mock)
local a = object( self )
a.mock = mock
a.replay_count = 0
a.min_replays = 1
a.max_replays = 1
return a
end
@
match and is_expected
---------------------
The [[match]] method takes an Action object as key for the search, because
it is a convenient way to pass all the needed properties. The Action
objects in the action list should compare themselves with the key to see if
they can replay such an action. The state properties of the key object are
of course not used, but the state of the actions in the list is important
for if the actions can be replayed or not. The method [[is_expected]]
examines if the state allows the action to be replayed. The [[match]]
method will be extended by the concrete Action class types.
<<Unit test for class Action.generic method is_expected>>=
function expect_unreplayed_action_test ()
assert_true( a:is_expected() )
end
<<Class Action.generic method is_expected>>=
function Action.generic:is_expected ()
return self.replay_count < self.max_replays
and not self.is_blocked
and not self.is_closed
end
<<Unit test for class Action.generic method match>>=
function match_unreplayed_test ()
assert_true( a:match( a ))
end
function match_rejects_replayed_action_test ()
a.replay_count = 1
assert_false( a:match( a ))
end
function match_rejects_wrong_action_type_test ()
-- Fake different type
local B = class( A )
local b = B:new()
assert_false( a:match( b ))
end
<<Class Action.generic method match>>=
function Action.generic:match (key)
if getmetatable(self) ~= getmetatable(key) then return false end
if self.mock ~= key.mock then return false end
return self:is_expected()
end
@
replay_action
-------------
The [[replay]] method updates the state of an Action. It has nothing to do
with performing the action, which is done by the Mock class and Callable
class. It is an internal error to call this method with an action that is
not expected.
<<Unit test for class Controller method replay_action>>=
function replay_action_test ()
local a = A:new()
mc:add_action( a )
assert_true( a:is_expected() )
assert_false( a:is_satisfied() )
mc:replay_action( a )
assert_false( a:is_expected() )
assert_true( a:is_satisfied() )
assert_equal( 1, a.replay_count )
end
<<Class Controller method replay_action>>=
function Controller:replay_action ( action )
assert( action:is_expected(), "lemock internal error" )
assert( action.replay_count < action.max_replays, "lemock internal error" )
local was_satisfied = action:is_satisfied()
action.replay_count = action.replay_count + 1
if not was_satisfied and action.labellist and action:is_satisfied() then
self:update_dependencies()
end
if action.closelist then
self:close_actions( action:closes() )
end
end
@
is_satisfied and assert_satisfied
---------------------------------
When the replay phase is finished the Controller needs to verify that all
the actions in the action list have been satisfied. If some action is not
satisfied it is an error, so [[assert_satisfied]] must also throw an
appropriate error.
<<Unit test for class Action.generic method is_satisfied>>=
function unreplayed_action_is_not_satisfied_test ()
assert_false( a:is_satisfied() )
end
function assert_satisfied_unreplayed_action_fails_test ()
local ok, err = pcall( function() a:assert_satisfied() end )
assert_false( ok, "unreplayed action was satisfied" )
assert_match( "Wrong replay count 0", err )
end
<<satisfied expression>>=
self.min_replays <= self.replay_count
<<Class Action.generic method is_satisfied>>=
function Action.generic:is_satisfied ()
return <<satisfied expression>>
end
<<Class Action.generic method assert_satisfied>>=
function Action.generic:assert_satisfied ()
assert( self.replay_count <= self.max_replays, "lemock internal error" )
if not (<<satisfied expression>>) then
error( sfmt( "Wrong replay count %d (expected %d..%d) for %s"
, self.replay_count
, self.min_replays, self.max_replays
, self:tostring()
)
, 0
)
end
end
@
Catching and Simulating Actions
###############################
Three classes work in tight connection to catch, record, and replay
actions. They are [[Controller]], [[Mock]], and [[Callable]].
The Controller is the main class, and it stores the action list with all
the Actions. The Controller is used to record explicit information that can
not be caught in an automatic manner by the Mock or Callable objects, such
as retur values. It is used to record meta information such as replay
limits and replay order dependencies. It is used to create the Mock
objects, to switch to replay mode, and to verify the completeness of the
replay phase.
The Mock must be completely empty of methods and properties, because we
want to catch all of them, without accidentally shadowing any names. So it
can only use Lua meta methods, like [[__index]] and [[__newindex]]. Because
of this limitation it is not possible to store a reference to the
Controller in the Mock object, so we need a map for this.
<<Module mock private data mock_controller_map>>=
local mock_controller_map = setmetatable( {}, {__mode='k'} )
@
When a method of a Mock object is called there are actually two actions.
First the method's name is indexed, and then the returned object is called.
So the Mock [[__index]] meta method must record the index part, *and* return
something with a [[__call]] meta method that can record the call part. This
something is a [[Callable]] object.
Setup Phase
===========
The Controller
--------------
To keep the user interface clean the Controller class itself is never
exported. The module function [[controller]] creates a new local controller
object, and creates and returns a wrapper for it with the exported methods.
<<Module mock function controller>>=
function controller ()
local exported_methods = {
'anytimes',
'atleastonce',
'close',
'depend',
'error',
'label',
'mock',
'new',
'replay',
'returns',
'times',
'verify',
}
local mc = Controller:new()
local wrapper = {}
for _, method in ipairs( exported_methods ) do
wrapper[ method ] = function (self, ...)
return mc[ method ]( mc, ... )
end
end
wrapper.ANYARG = Argv.ANYARG
wrapper.ANYARGS = Argv.ANYARGS
return wrapper
end
<<Class Controller method new>>=
function Controller:new ()
local mc = object( self )
mc.actionlist = {}
mc.is_recording = true
return mc
end
@
The Mock
--------
The Mock class has two modes, one for recording and one for replaying.
These two modes are implemented as two different metatables. The [[class]
helper functions is not used to create them, because it sets [[__index]] to
be a self reference. The [[__index]] meta method, and other meta methods,
of the two Mock metatables are used for catching actions, and are defined
in the corresponding action source files.
<<Class Mock meta tables Mock.record and Mock.replay>>=
Mock = { record={}, replay={} } -- no self-referencing __index!
@
Because the Mock must be completely empty it can not have a [[new]] method.
So the Controller has a [[mock]] method that creates and returns new mock
objects, and maps them to itself in [[mock_controller_map]]. The Mock is
created in record mode, but will be switched to replay mode later by the
Controller.
<<Unit test for module mock; mock creation>>=
function create_completely_empty_mock_test ()
for k, v in pairs( m ) do
fail( "Mock should be empty but contains "..tostring(k) )
end
end
function create_mock_during_replay_fails_test ()
mc:replay()
local ok, err = pcall( function() mc:mock() end )
assert_false( ok, "mock() succeeded" )
assert_match( "New mock during replay.", err )
end
<<Class Controller method mock>>=
function Controller:mock ()
if not self.is_recording then
error( "New mock during replay.", 2 )
end
local m = object( Mock.record )
mock_controller_map[m] = self
return m
end
@
The Callable
------------
A Callable is only created as the result of an index action. It is not
known at the time of the index action if the index will be called, but if
it is, the call action must update the index action with this information,
and therefore needs a reference to it.
<<Class Callable.generic method new>>=
function Callable.generic:new ( index_action )
local f = object( self )
f.action = index_action
return f
end
@
Record Phase
============
Return Values and Callables
---------------------------
Index action can either have a return value or be callable, in which case
they return a Callable object. When the call action is recorded the
corresponding index action must be marked as [[is_callable]], so that it
knows it shall return a Callable object during replay mode.
Index actions that return callables have their replay count limits set to
any-times, because all control adjustments will be made to the call action,
and it should not matter how many times the callable is retrieved from the
mock, but only how many times and in what order it is called.
If the retrieval count and/or order is important it is still possible to
simulate such behavior with an index action returning a mock object. Then
the index action can be controlled, and the call action can instead be
recorded with the selfcall action of the returned mock object.
<<Unit test for module mock; make callable>>=
function returns_on_empty_list_fails_test ()
local ok, err = pcall( function() mc:returns(nil) end )
assert_false( ok, "returns called on nothing" )
assert_match( "No action is recorded yet.", err )
end
function returns_make_call_fail_test ()
local tmp = m.foo ;mc:returns(1)
local ok, err = pcall( function() tmp(2) end )
assert_false( ok, "called index with returnvalue" )
assert_match( "Can not call foo. It has a returnvalue.", err )
end
function callable_index_replays_anytimes_test ()
local tmp = m.foo()
mc:replay()
tmp = m.foo
tmp = m.foo
tmp = m.foo()
mc:verify()
end
<<Class Controller method make_callable>>=
function Controller:make_callable (action)
if action.has_returnvalue then
error( "Can not call "..action.key..". It has a returnvalue.", 0 )
end
action.is_callable = true
action.min_replays = 0
action.max_replays = math.huge
end
@
Action classes that can return a value have the [[can_return]] property
set, and they implement the [[set_returnvalue]] and [[get_returnvalue]]
methods.
If an index action is marked as [[is_callable]] it ought to be followed by
a call action responsible for making it callable, and since it is no longer
the last action it should be impossible to set a returnvalue for the index
action. Doing so is an internal error.
<<Unit test for module mock returnvalue>>=
function returns_during_replay_fails_test ()
local tmp = m.foo
mc:replay()
local ok, err = pcall( function() mc:returns(1) end )
assert_false( ok, "returns() succeeded during replay" )
assert_match( "Returns called during replay.", err )
end
function returns_on_nonreturning_action_fails_test ()
m.foo = 1 -- assignments can't return
local ok, err = pcall( function() mc:returns(0) end )
assert_false( ok, "returns() succeeded on non-returning action" )
assert_match( "Previous action can not return anything.", err )
end
function returns_twice_fails_test ()
local tmp = m.foo ;mc:returns(1)
local ok, err = pcall( function() mc:returns(2) end )
assert_false( ok, "duplicate returns() succeeded" )
assert_match( "Returns and/or Error called twice for same action.", err )
end
<<Class Controller method returns>>=
function Controller:returns (...)
if not self.is_recording then
error( "Returns called during replay.", 2 )
end
local action = self:get_last_action()
assert( not action.is_callable, "lemock internal error" )
if not action.can_return then
error( "Previous action can not return anything.", 2 )
end
if action.has_returnvalue or action.throws_error then
error( "Returns and/or Error called twice for same action.", 2 )
end
action:set_returnvalue(...)
return self -- for chaining
end
@
Throwing Errors
---------------
Any action can be made to throw a recorded error when replayed. For
example, a newindex action could simulate an assignment to a userdata och
table with a metatable that is supposed to throw an error on assignments.
<<Unit test for module mock error>>=
function error_during_replay_fails_test ()
local tmp = m.foo
mc:replay()
local ok, err = pcall( function() mc:error(1) end )
assert_false( ok, "error() succeeded during replay" )
assert_match( "Error called during replay.", err )
end
function error_twice_fails_test ()
local tmp = m.foo ;mc:error(1)
local ok, err = pcall( function() mc:error(2) end )
assert_false( ok, "duplicate error() succeeded" )
assert_match( "Returns and/or Error called twice for same action.", err )
end
function error_plus_returns_fails_test ()
local tmp = m.foo ;mc:returns(1)
local ok, err = pcall( function() mc:error(2) end )
assert_false( ok, "both error and returns succeeded" )
assert_match( "Returns and/or Error called twice for same action.", err )
end
<<Class Controller method error>>=
function Controller:error (value)
if not self.is_recording then
error( "Error called during replay.", 2 )
end
local action = self:get_last_action()
if action.has_returnvalue or action.throws_error then
error( "Returns and/or Error called twice for same action.", 2 )
end
action.throws_error = true
action.errorvalue = value
return self -- for chaining
end
@
Switching from Record Mode to Replay Mode
=========================================
Switching to replay mode is done by changing all Mock objects' matatables.
It is an error to call [[replay]] twice on the same Controller.
Dependency information (see restrictions.nw) is not calculated in real
time. It is updated when needed, for example before starting the replay
phase. This is also a good point to check for dependency cycles.
<<Unit test for module mock; switching to replay mode>>=
function replay_twice_fails_test ()
mc:replay()
local ok, err = pcall( function() mc:replay() end )
assert_false( ok, "replay succeeded twice" )
assert_match( "Replay called twice.", err )
end
function multiple_controllers_test ()
local mc2 = lemock.controller()
local m2 = mc2:mock()
-- m -- -- m2 --
m.foo = 1
mc:replay()
m2.bar = 2
m.foo = 1
mc2:replay()
mc:verify()
m2.bar = 2
mc2:verify()
end
<<Unit test for class Controller method replay>>=
function replay_test ()
assert_true( mc.is_recording )
mc:replay()
assert_false( mc.is_recording )
end
<<Class Controller method replay>>=
function Controller:replay ()
if not self.is_recording then
error( "Replay called twice.", 2 )
end
self.is_recording = false
for m, mc in pairs( mock_controller_map ) do
if mc == self then
setmetatable( m, Mock.replay )
end
end
self:update_dependencies()
self:assert_no_dependency_cycles()
end
@
Replay Phase
============
The key point with the replay phase is that unexpected actions should fail.
<<Unit test for module mock; replay>>=
function replay_in_any_order_test ()
m.a = 1
m.b = 2
m.c = 3
mc:replay()
m.c = 3
m.a = 1
m.b = 2
mc:verify()
end
function replaying_unexpected_action_fails_test ()
mc:replay()
local ok, err = pcall( function() m:somethingelse() end )
assert_false( ok, "unexpected replay succeeded" )
assert_match( "Unexpected action index somethingelse", err )
end
@
There is an error that would be very hard to track down if it is not
detected. During record mode the Mock returns recording Callables, but
during replay mode it returns replaying Callables. If the client caches a
Callable in record mode and uses it during replay mode, it would not fail
straight away as desired (if undetected), but record a false action which
would mess up the action list and cause some later action or the final
verify to pass or fail with a perplexing error message.
<<Unit test for module mock; replay>>=
function cached_recording_callable_fails_during_replay_test ()
local tmp = m.foo ; tmp()
mc:replay()
local ok, err = pcall( function() tmp() end )
assert_false( ok, "Cached callable not detected" )
assert_match( "client uses cached callable from recording", err )
end
@
Verify Completeness Phase
=========================
Unknown or misplaced actions can be detected during the replay phase, but
expected actions that are not replayed can not be detected until the replay
phase is finished. That is why the verify phase is needed, and it simply
asserts that all recorded actions have been satisfied.
<<Unit test for module mock; verify>>=
function verify_during_record_phase_fails_test ()
local ok, err = pcall( function() mc:verify() end )
assert_false( ok, "Verify succeeded" )
assert_match( "Verify called during record.", err )
end
function verify_replayed_actionlist_test ()
mc:replay()
mc:verify()
end
function verify_unreplyed_actionlist_fails_test ()
local tmp = m.foo
mc:replay()
local ok, err = pcall( function() mc:verify() end )
assert_false( ok, "Verify succeeded" )
assert_match( "Wrong replay count 0 ", err )
end
<<Class Controller method verify>>=
function Controller:verify ()
if self.is_recording then
error( "Verify called during record.", 2 )
end
for a in self:actions() do
a:assert_satisfied()
end
end