1 -*- mode: markdown; coding: utf-8-unix; -*-
3 CL-MOCK - Mocking functions.
5 Copyright (C) 2013-14 Olof-Joachim Frahm
7 Release under a Simplified BSD license.
9 Working, but unfinished.
16 This small library provides a way to replace the actual implementation
17 of either regular or generic functions with mocks. On the one hand how
18 to integrate this facility with a testing library is up to the user; the
19 tests for the library are written in [`FIVEAM`][2] though, so most
20 examples will take that into account. On the other hand writing
21 interactions for mocks usually relies on a bit of pattern matching,
22 therefore the regular `CL-MOCK` package relies on [`OPTIMA`][3] to
23 provide that facility instead of deferring to the user. Should this be
24 a concern a reduced system definition is available as `CL-MOCK-BASIC`,
25 which excludes the definition of `ANSWER` and the dependency on
28 Since it is pretty easy to just roll something like this on your own,
29 the main purpose is to develop a nice (lispy, declarative) syntax to
30 keep your tests readable and maintainable.
32 Some parts may be used independently of the testing facilities,
33 e.g. dynamic `FLET` may be of general interest.
36 # MOCKING REGULAR FUNCTIONS
38 Let's say we have a function `FOO`, then we can replace it for testing
39 by establishing a new mocking context and then specifying how the new
40 function should behave (see below in **UTILITIES** for a more primitive
41 dynamic function rebinding):
43 > (declaim (notinline foo bar))
45 > (defun bar (&rest args)
46 > (declare (ignore args))
53 > (eql 23 (foo 'bar))))
56 The `ANSWER` macro has pattern matching (see [`OPTIMA`][3]) integrated.
57 Therefore something like the following will now work as expected:
60 > (answer (foo x) (format T "Hello, ~A!" x))
64 If you don't like `ANSWER` as it is, you can still use `IF-CALLED`
65 directly. Note however that unless `UNHANDLED` is called, the function
66 always matches and the return value is directly returned again:
69 > (if-called 'foo (lambda (x)
71 > (error "Not executed!")))
72 > (if-called 'foo (lambda (x) (format T "Hello, ~A!" x)))
76 Be especially careful to handle all given arguments, otherwise the
77 function call will fail and that error is propagated upwards.
79 `IF-CALLED` also has another option to push a binding to the front of
80 the list, which (as of now) isn't available via `ANSWER` (and should be
81 treated as subject to change anyway).
83 Should you wish to run the previously defined function, use the function
84 `CALL-PREVIOUS`. If no arguments are passed it will use the current
85 arguments from `*ARGUMENTS*`, if any. Otherwise it will be called with
86 the passed arguments instead. For cases where explicitely calling it
87 with no arguments is necessary, using `(funcall *previous*)` is still
91 > (answer foo `(was originally ,(funcall *previous*)))
92 > (answer bar `(was originally ,(call-previous)))
96 > => (WAS ORIGINALLY FOO) (WAS ORIGINALLY BAR)
98 The function `INVOCATIONS` may be used to retrieve all recorded
99 invocations of mocks (so far); the optional argument can be used to
100 filter for a particular name:
107 > (invocations 'foo))
111 Currently there are no further predicates to check these values, this is
112 however an area of investigation, so presumably either a macro like
113 [`FIVEAM`][2]s `IS`, or regular predicates could appear in this place.
118 The following examples may give a better impression.
120 Here we test a particular [`ECLASTIC`][4] method, `GET*`. In order to
121 replace the HTTP call with a supplied value, we use `ANSWER` with
122 `HTTP-REQUEST` and return a pre-filled stream. Afterwards both the
123 number of `INVOCATIONS` and the actual returned values are checked.
125 (use-package '(#:cl-mock #:fiveam #:eclastic #:drakma #:puri))
127 (def-test search.empty ()
128 (let* ((events (make-instance '<type> :type "document" :index "index"
129 :host "localhost" :port 9292))
130 (text "{\"took\":3,\"timed_out\":false,\"_shards\":{\"total\":5,\
131 \"successful\":5,\"failed\":0},\"hits\":{\"total\":123,\"max_score\":1.0,\
132 \"hits\":[{\"_index\":\"index\",\"_type\":\"document\",\"_id\":\"12345\",\
133 \"_score\":1.0,\"_source\":{\"test\": \"Hello, World!\"}}]}}")
134 (stream (make-string-input-stream text)))
137 (values stream 200 NIL
138 (parse-uri "http://localhost:9292/index/document/_search")
140 (let ((values (multiple-value-list
141 (get* events (new-search NIL)))))
142 (is (eql 1 (length (invocations))))
143 (is (eql 1 (length (car values))))
144 (is-true (typep (caar values) '<document>))
145 (is (equal (cdr values)
147 :shards (:total 5 :failed 0 :successful 5)
148 :timed-out NIL :took 3))))))))
150 Of course, running this should produce no errors:
152 > (run! 'search.empty)
154 > Running test SEARCH.EMPTY ....
165 `DFLET` dynamically rebinds functions similar to `FLET`:
168 > (defun bar () (foo))
171 > (dflet ((foo () 23))
174 > (OR) => 42, if FOO was inlined
176 The caveat is that this might not work on certain optimisation settings,
177 including inlining. That trade-off seems acceptable; it would be nice
178 if a warning could be issued depending on the current optimisation
179 settings, however that is particularly implementation dependent, so lack
180 of a warning won't indicate a working environment.
182 The underlying function `PROGF` may be used as well similarly to the
185 > (progf '(foo) (list (lambda () 23))
188 > (OR) => 42, if FOO was inlined
190 [1]: http://common-lisp.net/project/closer/closer-mop.html
191 [2]: http://common-lisp.net/project/fiveam/
192 [3]: https://github.com/m2ym/optima
193 [4]: https://github.com/gschjetne/eclastic