mimock
Mimock (mini mock) is a small simple mocking library with a low learning curve.
Use guide
Test scenarios are illustrated with mocha/karma it
and jasmine/chai
style expect
assertions.
Import
Import the mockset
constructor as follows:
let mimock = ;let mockset = mimockmockset;
The mockset
constructor creates objects which are essentially a set of mocks.
When mocking behaviour is introduced, it is done via the mock set object, and
this makes it easy to undo.
Creating a complete new object type
let mocks = ; let play_object = mockset;
Now, objects of this type may be created as usual:
let ball = colour: 'blue' foo: 'round' bar: 'bouncy' ; let ball_foo = ball;let old_ball_bar = ball;let ball_bar = ball;
Constructor and methods are optional.
original state just by calling restore()
on it.
Object instrumentation
You can instrument an object method like this (o
can be used instead
of object and m
instead of method
):
let mocks = ;let some_object = ;let some_method = mocksobjectsome_objectmethod'some_method';
That changes the some_method
call on some_object
so that calls to
the target method are 'observed' from then on.
The number of times the method has been called (after observation start)
is then available by calling some_method.call_count()
. The calls themselves
can be accessed by calling some_method.calls()
, which returns an array of
objects (sorted be 'called'), each object having the following keys:
Key | Description |
---|---|
called | A Date object; when the call was made |
returned | A Date object; when the call returned |
args | An array comprising the arguments the caller passed |
retval | The return value, included unless it threw an exception |
exception | An exception value, if it threw an exception |
A call counting test can be done using a wrap, like this test that checks a method on an object is called twice:
;
Instrumention allows you to do it like this however:
;
Instrumentation can be cancelled either by calling some_method.restore()
, or
mocks.restore()
will cancel all wrappers, and anything else, done with
mocks
.
Object method wrapping
A wrapper is a function that is called instead of an objects usual method. The wrapper can effectively stub the call by returning some value of its choosing to the caller, or invoke the original method (with or without modified arguments, and optionally modifying the return value), or it could throw an exception. Because the wrapper is invoked instead of the original method it can really do anything it wants.
The objects original method is referred to below as the target method.
Method stubbing wrappers
This is the simplest possible type of wrapper; here this target method is
replaced with this function that always just returns 30
(w
can be used
instead of wrap
):
let mocks = ; let some_method = mocksobjectsome_objectmethod'some_method';let wrap = some_method; mocks;
Method pass through wrappers
The wrapper is passed an argument (helper
in the examples
below), providing access to various things, including the original/target
function. A helper method called continue
causes the target function
to be called (with whatever arguments were passed by the caller).
The return value is whatever is returned by the original/target. A
completely benign wrapper could be applied like this therefore:
let mocks = ; let some_method = mocksobjectsome_objectmethod'some_method';some_method; mocks;
The arguments, conspicuously missing above, are accessible as an
array, helper.args
, and modifying this array causes different
arguments to be passed on to the target method when continue
is
called.
So, to cause the target method to be invoked with the first argument multipled by ten, you could do this:
let mocks = ; let some_method = mocksobjectsome_objectmethod'some_method';some_method; mocks;
Or you could throw an exception if the second argument is equal "dog":
let mocks = ; let some_method = mocksobjectsome_objectmethod'some_method';some_method; mocks;
Modifying the return value is probably fairly obvious, here the return value is multipled by five:
let mocks = ; let some_method = mocksobjectsome_objectmethod'some_method';let wrap = some_method; mocks;
The wrapper can be cancelled. Calling mocks.restore()
will cancel
everything created with the mocks
object, some_method.unwrap(function)
or wrap.restore()
will remove just that wrapper function, leaving
any other wraps and instrumentation operable, or some_method.restore()
will return that method to original service (all wrappers removed and
no instrumentation).
Layered wrappers
What if you wrap a method already wrapped?
The answer is, it's wrapped again. The new wrapper is called first (think of it
a present you're wrapping in layers of wrapping paper, the latest layer goes on
top) then the previously set wrapper, then the real function (assuming both
wrappers call helper.continue()
of course). If the first (to be called)
wrapper modifies the arguments, the second gets the modifications and sees
nothing of the originals. If the first returns (as a stub), or throws an
exception, the second wrapper will never see the light of day.
Wrapping a function
A function, not on an object, can't be usefully changed as such, if something already has a reference to it there's not much you can do, but it can be substituted:
let mocks = ; let orig_fun = ;let some_fun_mock = mocks
Calling replacement()
will provide a replacement function which passes
control to the original after instrumentation:
let new_fun = some_fun_mock;let fun_wrap = some_fun_mock;;; mocks;
The call history (when, args, return value, etc) is also available by
calling calls()
.
Instrumentation and all wraps are cancelled by calling
some_fun_mock.restore()
, that one wrap can be removed with
some_fun_mock.unwrap(function)
or fun_wrap.restore()
, or everything
done with mocks
can be cancelled with mocks.restore()
.
Wrapping a module
If you want to wrap a function on an object that another function creates
that's a bit of a problem, because you never get your hands on the object
to instrument or wrapper it. You can solve this (probably) by wrapping the
whole module/library (these examples use real existing modules testob
(a test object module) and polylock
(a multiple concurrent resource lock
module)):
let mocks = ; let testob_lib = mocks;
Once you've done this, first the library is instrumented, so you can check how many times it has been required:
let testob = ;; mocks;
Bits of the module can now be wrapped. Modules can be anything of course, some export a function, and others export an object, often with functions in/on it, though it is perfectly possible for a module to export a number or string too. You must tell mimock which elements of a module's exports you want to mock.
Modules exporting a function
If the module exports a function (such as polylock
or PouchDB
) do
this (note l
can be used instead of library
):
let mocks = ; let polylock_lib = mocks;let root_export = polylock_lib;
The 'root' export, polylock's constructor, is now instrumented, and you can wrap it just like functions or object methods:
let polylock = ; let locks = ;;root_export;
Now if you want to wrap methods on the objects constructed you
can (here the test_locks
methods on all new objects is wrapped):
let polylock = ; let test_locks_method;root_export; let locks = ;not;;try let retval = locks; console; catch err console;; mocks;
Modules exporting an object
If the module exports an object (such as testob
) do this:
let mocks = ; let testob_lib = mocks;let basic_export = testob_lib;
The 'basic' export, references the 'basic' element from the testob module exports. It is now (as of the above) instrumented, and you can wrap it:
let testob = ;let testob_basic = testobbasic; let basic_object = ;;basic_export;
Wrapping methods on the objects constructed is the same as above, for modules exporting a function.
Module wrapping behaviours
Any module required before a call to (new mockset()).library(name)
is
entirely free from any Mimock influence.
Any module required after a call to (new mockset()).library(name)
will
be affected.
Where a module exports a function, the function is replaced (and instrumented) even if you don't ask for this. The reason this is done is to ensure consistent behaviour as compared with modules that expport an object... generally the principals of engineering here are that the order things are done in should have as little impact as possible (i.e. A then B yields the same result as B then A), and that performing an operation on X should have the same (context allowing) impact as the same operation on Y...
...thus where a module exports an object with functions on/in, those
functions can be changed later, substituted within the object, but where
a module exports a function, this can't be done... so the function is
wrapped regardless so that later a call to lib.e(undefined)
will work
as well as lib.e('name_of_function')
.