Better invocation recording.
[cl-mock.git] / README.md
1 -*- mode: markdown; coding: utf-8-unix; -*-
2
3 CL-MOCK - Mocking functions.
4
5 Copyright (C) 2013-15 Olof-Joachim Frahm
6
7 Release under a Simplified BSD license.
8
9 Working, but unfinished.
10
11 [![Build Status](https://travis-ci.org/Ferada/cl-mock.svg?branch=master)](https://travis-ci.org/Ferada/cl-mock)
12
13 Portable to at least ABCL, Allegro CL (with one problem with inlining
14 settings), SBCL, CCL and CLISP.  CMUCL possibly, but not tested on
15 Travis CI.  ECL fails on Travis CI ([`OPTIMA`][3] fails there as well),
16 but runs successfully on my own machine, so YMMV.  See the detailed
17 reports at
18 [https://travis-ci.org/Ferada/cl-mock](https://travis-ci.org/Ferada/cl-mock)
19 for more information and [`CL-TRAVIS`][5], and [`.travis.yml`][6] for the
20 setup.
21
22
23 # INTRODUCTION
24
25 This small library provides a way to replace the actual implementation
26 of either regular or generic functions with mocks.  On the one hand how
27 to integrate this facility with a testing library is up to the user; the
28 tests for the library are written in [`FIVEAM`][2] though, so most
29 examples will take that into account.  On the other hand writing
30 interactions for mocks usually relies on a bit of pattern matching,
31 therefore the regular `CL-MOCK` package relies on [`OPTIMA`][3] to
32 provide that facility instead of deferring to the user.  Should this be
33 a concern a reduced system definition is available as `CL-MOCK-BASIC`,
34 which excludes the definition of `ANSWER` and the dependency on
35 [`OPTIMA`][3].
36
37 Since it is pretty easy to just roll something like this on your own,
38 the main purpose is to develop a nice (lispy, declarative) syntax to
39 keep your tests readable and maintainable.
40
41 Some parts may be used independently of the testing facilities,
42 e.g. dynamic `FLET` may be of general interest.
43
44
45 # MOCKING REGULAR FUNCTIONS
46
47 Let's say we have a function `FOO`, then we can replace it for testing
48 by establishing a new mocking context and then specifying how the new
49 function should behave (see below in **UTILITIES** for a more primitive
50 dynamic function rebinding):
51
52     > (declaim (notinline foo bar))
53     > (defun foo () 'foo)
54     > (defun bar (&rest args)
55     >   (declare (ignore args))
56     >   'bar)
57     > (with-mocks ()
58     >   (answer (foo 1) 42)
59     >   (answer foo 23)
60     >   (values
61     >    (eql 42 (foo 1))
62     >    (eql 23 (foo 'bar))))
63     > => T T
64
65 The `ANSWER` macro has pattern matching (see [`OPTIMA`][3]) integrated.
66 Therefore something like the following will now work as expected:
67
68     > (with-mocks ()
69     >   (answer (foo x) (format T "Hello, ~A!" x))
70     >   (foo "world"))
71     > => "Hello, world!"
72
73 If you don't like `ANSWER` as it is, you can still use `IF-CALLED`
74 directly.  Note however that unless `UNHANDLED` is called, the function
75 always matches and the return value is directly returned again:
76
77     > (with-mocks ()
78     >   (if-called 'foo (lambda (x)
79     >                     (unhandled)
80     >                     (error "Not executed!")))
81     >   (if-called 'foo (lambda (x) (format T "Hello, ~A!" x)))
82     >   (foo "world"))
83     > => "Hello, world!"
84
85 Be especially careful to handle all given arguments, otherwise the
86 function call will fail and that error is propagated upwards.
87
88 `IF-CALLED` also has another option to push a binding to the front of
89 the list, which (as of now) isn't available via `ANSWER` (and should be
90 treated as subject to change anyway).
91
92 Should you wish to run the previously defined function, use the function
93 `CALL-PREVIOUS`.  If no arguments are passed it will use the current
94 arguments from `*ARGUMENTS*`, if any.  Otherwise it will be called with
95 the passed arguments instead.  For cases where explicitely calling it
96 with no arguments is necessary, using `(funcall *previous*)` is still
97 possible as well.
98
99     > (with-mocks ()
100     >   (answer foo `(was originally ,(funcall *previous*)))
101     >   (answer bar `(was originally ,(call-previous)))
102     >   (values
103     >    (foo "hello")
104     >    (bar "hello")))
105     > => (WAS ORIGINALLY FOO) (WAS ORIGINALLY BAR)
106
107 The function `INVOCATIONS` may be used to retrieve all recorded
108 invocations of mocks (so far); the optional argument can be used to
109 filter for a particular name:
110
111     > (with-mocks ()
112     >   (answer foo)
113     >   (foo "hello")
114     >   (foo "world")
115     >   (bar "test")
116     >   (invocations 'foo))
117     > => ((FOO "hello")
118     >     (FOO "world"))
119
120 Currently there are no further predicates to check these values, this is
121 however an area of investigation, so presumably either a macro like
122 [`FIVEAM`][2]s `IS`, or regular predicates could appear in this place.
123
124
125 # EXAMPLES
126
127 The following examples may give a better impression.
128
129 Here we test a particular [`ECLASTIC`][4] method, `GET*`.  In order to
130 replace the HTTP call with a supplied value, we use `ANSWER` with
131 `HTTP-REQUEST` and return a pre-filled stream.  Afterwards both the
132 number of `INVOCATIONS` and the actual returned values are checked.
133
134     (use-package '(#:cl-mock #:fiveam #:eclastic #:drakma #:puri))
135
136     (def-test search.empty ()
137       (let* ((events (make-instance '<type> :type "document" :index "index"
138                                             :host "localhost" :port 9292))
139              (text "{\"took\":3,\"timed_out\":false,\"_shards\":{\"total\":5,\
140     \"successful\":5,\"failed\":0},\"hits\":{\"total\":123,\"max_score\":1.0,\
141     \"hits\":[{\"_index\":\"index\",\"_type\":\"document\",\"_id\":\"12345\",\
142     \"_score\":1.0,\"_source\":{\"test\": \"Hello, World!\"}}]}}")
143              (stream (make-string-input-stream text)))
144         (with-mocks ()
145           (answer http-request
146             (values stream 200 NIL
147                     (parse-uri "http://localhost:9292/index/document/_search")
148                     stream NIL "OK"))
149           (let ((values (multiple-value-list
150                          (get* events (new-search NIL)))))
151             (is (eql 1 (length (invocations))))
152             (is (eql 1 (length (car values))))
153             (is-true (typep (caar values) '<document>))
154             (is (equal (cdr values)
155                        '(NIL (:hits 123
156                               :shards (:total 5 :failed 0 :successful 5)
157                               :timed-out NIL :took 3))))))))
158
159 Of course, running this should produce no errors:
160
161     > (run! 'search.empty)
162     >
163     > Running test SEARCH.EMPTY ....
164     > Did 4 checks.
165     >    Pass: 4 (100%)
166     >    Skip: 0 ( 0%)
167     >    Fail: 0 ( 0%)
168     >
169     > => NIL
170
171
172 # UTILITIES
173
174 `DFLET` dynamically rebinds functions similar to `FLET`:
175
176     > (defun foo () 42)
177     > (defun bar () (foo))
178     > (bar)
179     > => 42
180     > (dflet ((foo () 23))
181     >   (bar))
182     > => 23
183     > (OR) => 42, if FOO was inlined
184
185 The caveat is that this might not work on certain optimisation settings,
186 including inlining.  That trade-off seems acceptable; it would be nice
187 if a warning could be issued depending on the current optimisation
188 settings, however that is particularly implementation dependent, so lack
189 of a warning won't indicate a working environment.
190
191 The underlying function `PROGF` may be used as well similarly to the
192 standard `PROG`:
193
194     > (progf '(foo) (list (lambda () 23))
195     >   (bar))
196     > => 23
197     > (OR) => 42, if FOO was inlined
198
199 [1]: http://common-lisp.net/project/closer/closer-mop.html
200 [2]: http://common-lisp.net/project/fiveam/
201 [3]: https://github.com/m2ym/optima
202 [4]: https://github.com/gschjetne/eclastic
203 [5]: https://github.com/luismbo/cl-travis
204 [6]: https://raw.githubusercontent.com/Ferada/cl-mock/master/.travis.yml