LeMock

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.

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.

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.

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

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.

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).

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.

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.