cl-mock
2022-11-07
Mocking library
-- mode: markdown; coding: utf-8-unix; --
CL-MOCK - Mocking functions.
Copyright (C) 2013-16 Olof-Joachim Frahm
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/.
Working, but unfinished.
Portable to at least ABCL, Allegro CL (with one problem with inlining
settings), SBCL, CCL and CLISP. CMUCL possibly, but not tested on
Travis CI. ECL fails on Travis CI (TRIVIA
fails there as well),
but runs successfully on my own machine, so YMMV. See the detailed
reports at
https://travis-ci.org/Ferada/cl-mock
for more information and CL-TRAVIS
, and .travis.yml
for the
setup.
INTRODUCTION
This small library provides a way to replace the actual implementation
of either regular or generic functions with mocks. On the one hand how
to integrate this facility with a testing library is up to the user; the
tests for the library are written in FIVEAM
though, so most
examples will take that into account. On the other hand writing
interactions for mocks usually relies on a bit of pattern matching,
therefore the regular CL-MOCK
package relies on TRIVIA
to
provide that facility instead of deferring to the user. Should this be
a concern a reduced system definition is available as CL-MOCK-BASIC
,
which excludes the definition of ANSWER
and the dependency on
TRIVIA
.
Since it is pretty easy to just roll something like this on your own, the main purpose is to develop a nice (lispy, declarative) syntax to keep your tests readable and maintainable.
Some parts may be used independently of the testing facilities,
e.g. dynamic FLET
may be of general interest.
MOCKING REGULAR FUNCTIONS
Let's say we have a function FOO
, then we can replace it for testing
by establishing a new mocking context and then specifying how the new
function should behave (see below in UTILITIES for a more primitive
dynamic function rebinding):
> (declaim (notinline foo bar))
> (defun foo () 'foo)
> (defun bar (&rest args)
> (declare (ignore args))
> 'bar)
> (with-mocks ()
> (answer (foo 1) 42)
> (answer foo 23)
> (values
> (eql 42 (foo 1))
> (eql 23 (foo 'bar))))
> => T T
The ANSWER
macro has pattern matching (see TRIVIA
) integrated.
Therefore something like the following will now work as expected:
> (with-mocks ()
> (answer (foo x) (format T "Hello, ~A!" x))
> (foo "world"))
> => "Hello, world!"
If you don't like ANSWER
as it is, you can still use IF-CALLED
directly. Note however that unless UNHANDLED
is called, the function
always matches and the return value is directly returned again:
> (with-mocks ()
> (if-called 'foo (lambda (x)
> (unhandled)
> (error "Not executed!")))
> (if-called 'foo (lambda (x) (format T "Hello, ~A!" x)))
> (foo "world"))
> => "Hello, world!"
Be especially careful to handle all given arguments, otherwise the function call will fail and that error is propagated upwards.
IF-CALLED
also has another option to push a binding to the front of
the list, which (as of now) isn't available via ANSWER
(and should be
treated as subject to change anyway).
Should you wish to run the previously defined function, use the function
CALL-PREVIOUS
. If no arguments are passed it will use the current
arguments from *ARGUMENTS*
, if any. Otherwise it will be called with
the passed arguments instead. For cases where explicitely calling it
with no arguments is necessary, using (funcall *previous*)
is still
possible as well.
> (with-mocks ()
> (answer foo `(was originally ,(funcall *previous*)))
> (answer bar `(was originally ,(call-previous)))
> (values
> (foo "hello")
> (bar "hello")))
> => (WAS ORIGINALLY FOO) (WAS ORIGINALLY BAR)
The function INVOCATIONS
may be used to retrieve all recorded
invocations of mocks (so far); the optional argument can be used to
filter for a particular name:
> (with-mocks ()
> (answer foo)
> (foo "hello")
> (foo "world")
> (bar "test")
> (invocations 'foo))
> => ((FOO "hello")
> (FOO "world"))
Currently there are no further predicates to check these values, this is
however an area of investigation, so presumably either a macro like
FIVEAM
s IS
, or regular predicates could appear in this place.
EXAMPLES
The following examples may give a better impression.
Here we test a particular ECLASTIC
method, GET*
. In order to
replace the HTTP call with a supplied value, we use ANSWER
with
HTTP-REQUEST
and return a pre-filled stream. Afterwards both the
number of INVOCATIONS
and the actual returned values are checked.
(use-package '(#:cl-mock #:fiveam #:eclastic #:drakma #:puri))
(def-test search.empty ()
(let* ((events (make-instance '<type> :type "document" :index "index"
:host "localhost" :port 9292))
(text "{\"took\":3,\"timed_out\":false,\"_shards\":{\"total\":5,\
\"successful\":5,\"failed\":0},\"hits\":{\"total\":123,\"max_score\":1.0,\
\"hits\":[{\"_index\":\"index\",\"_type\":\"document\",\"_id\":\"12345\",\
\"_score\":1.0,\"_source\":{\"test\": \"Hello, World!\"}}]}}")
(stream (make-string-input-stream text)))
(with-mocks ()
(answer http-request
(values stream 200 NIL
(parse-uri "http://localhost:9292/index/document/_search")
stream NIL "OK"))
(let ((values (multiple-value-list
(get* events (new-search NIL)))))
(is (eql 1 (length (invocations))))
(is (eql 1 (length (car values))))
(is-true (typep (caar values) '<document>))
(is (equal (cdr values)
'(NIL (:hits 123
:shards (:total 5 :failed 0 :successful 5)
:timed-out NIL :took 3))))))))
Of course, running this should produce no errors:
> (run! 'search.empty)
>
> Running test SEARCH.EMPTY ....
> Did 4 checks.
> Pass: 4 (100%)
> Skip: 0 ( 0%)
> Fail: 0 ( 0%)
>
> => NIL
UTILITIES
DFLET
dynamically rebinds functions similar to FLET
:
> (defun foo () 42)
> (defun bar () (foo))
> (bar)
> => 42
> (dflet ((foo () 23))
> (bar))
> => 23
> (OR) => 42, if FOO was inlined
The caveat is that this might not work on certain optimisation settings, including inlining. That trade-off seems acceptable; it would be nice if a warning could be issued depending on the current optimisation settings, however that is particularly implementation dependent, so lack of a warning won't indicate a working environment.
The underlying function PROGF
may be used as well similarly to the
standard PROG
:
> (progf '(foo) (list (lambda () 23))
> (bar))
> => 23
> (OR) => 42, if FOO was inlined