Mocker.el is a mocking framework for Emacs lisp.
Its single entry point,
mocker-let provides an
let like interface to
defining mock objects. Actually,
mocker-let is a wrapper around
can be seen as a way to manually generate mocks.
Let's start with a simple example:
(mocker-let ((foo (x y z) ((:input '(1 2 3) :output 4) (:input '(4 5 6) :output 10))) (bar (x) ((:input '(42) :output 4)))) (+ (foo 1 2 3) (foo 4 5 6) (bar 42)))
Each mock is defined in a function-style, and is associated with a set of "records" that map expected inputs to desired outputs.
By default, the order of definition within a mock has to be respected by the
wrapped code, so that in this situation it would be an error to observe
(foo 4 5 6) before
(foo 1 2 3).
(mocker-let ((foo (x y z) ((:input '(1 2 3) :output 4) (:input '(4 5 6) :output 10))) (bar (x) ((:input '(42) :output 4)))) (+ (foo 4 5 6) (foo 1 2 3) (bar 42)))
In such a situation, you'll get a typed error with a message like
(mocker-record-error "Violated record while mocking `foo'. Expected input like: `(1 2 3)', got: `(4 5 6)' instead") ...
If order is not important, you can obtain the same effect as before by specifying it:
(mocker-let ((foo (x y z) :ordered nil ((:input '(1 2 3) :output 4) (:input '(4 5 6) :output 10))) (bar (x) ((:input '(42) :output 4)))) (+ (foo 4 5 6) (foo 1 2 3) (bar 42)))
In many situations it can be pretty repetitive to list all the expected calls
to a mock. In some, the count might even be a range rather than a fixed number.
:max-occur options allow to tune that. By default, they
are both set to 1, so that exactly 1 call is expected. As a special case,
:max-occur to nil will accept any number of calls.
:occur shorthand is also provided, to expect an exact number of calls.
(mocker-let ((foo (x) ((:input '(1) :output 1 :min-occur 1 :max-occur 3)))) (+ (foo 1) (foo 1)))
This example will accept between 1 and 3 calls to
(foo 1), and complain if
that constraint is not fulfilled.
Note the applied algorithm is greedy, so that as many calls as possible will count as part of the earliest constraints.
The examples above are fine, but they suppose input and output are just constant expressions. A useful addition is the ability to match arbitrary input and generate arbitrary output.
To this end, the
:output-generator options can be used
instead (actually think of
:output as convenience shortcuts for
(mocker-let ((foo (x) :ordered nil ((:input-matcher 'oddp :output-generator 'identity :max-occur 2) (:input-matcher 'evenp :output 0)))) (+ (foo 1) (foo 2) (foo 3)))
:output-generator values need to be functions (or
function symbols) accepting the same arguments as the mocked function itself.
Each record definition actually builds a
mocker-record object, that's
responsible for checking the actual behavior. By providing alternative
implementations of those records, one can adapt the mocking to special needs.
As a quick proof of concept, an implementation of a stub is provided with the
mocker-stub-record which casualy ignores any input and always emits the
(mocker-let ((foo (x) ((:record-cls 'mocker-stub-record :output 42)))) (foo 12345))
In some occasions, you might want to mock only some calls for a function, and
let other calls invoke the real one. This can be achieved by using the
mocker-passthrough-record. In the following example, the first call to
ignore uses the real implementation, while the second one is mocked to return
(mocker-let ((ignore (x) :records ((:record-cls mocker-passthrough-record :input '(42)) (:input '(58) :output t)))) (or (ignore 42) (ignore 58)))
Provide your own
Customized classes can be provided, that can even introduce a mini-language for
describing the stub. This can be achieved by overloading
In case the customized record class is meant to be used in many tests, it might be more convenient to use a pattern like:
(let ((mocker-mock-default-record-cls 'mocker-stub-record)) (mocker-let ((foo (x) ((:output 42))) (bar (x y) ((:output 1)))) (+ (foo 12345) (bar 5 14))))
Also note that
mocker-stub-record set their
:min-occur to 0 and
:max-occur to nil, if not specified otherwise.
Comparison to other mocking solutions
el-mock.el uses a small DSL for recording behavior, which is great for conciseness. mocker.el instead uses regular lisp as much as possible, which is more flexible.
el-mock.el does not allow recording multiple behaviors (the same call will always return the same value). This makes it difficult to use in real situation, where different call sites for the same function might have to behave differently.
;;; automatically answer some `y-or-n-p' questions (mocker-let ((y-or-n-p (prompt) ((:input '("Really?") :output t) (:input '("I mean... for real?") :output nil)))) ...)
;;; blindly accept all `yes-or-no-p' questions (mocker-let ((yes-or-no-p (prompt) ((:record-cls mocker-stub-record :output t)))) ...)
;;; make `foo' generate the fibonacci suite, no matter how it's called (mocker-let ((foo (x) ((:input-matcher (lambda (x) t) :output-generator (lexical-let ((x 0) (y 1)) (lambda (any) (let ((z (+ x y))) (setq x y y z)))) :max-occur nil)))) ...)