rlm@10
|
1 ;;; clojure.contrib.mock.clj: mocking/expectation framework for Clojure
|
rlm@10
|
2
|
rlm@10
|
3 ;; by Matt Clark
|
rlm@10
|
4
|
rlm@10
|
5 ;; Copyright (c) Matt Clark, 2009. All rights reserved. The use
|
rlm@10
|
6 ;; and distribution terms for this software are covered by the Eclipse
|
rlm@10
|
7 ;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php).
|
rlm@10
|
8 ;; By using this software in any fashion, you are
|
rlm@10
|
9 ;; agreeing to be bound by the terms of this license. You must not
|
rlm@10
|
10 ;; remove this notice, or any other, from this software.
|
rlm@10
|
11 ;;------------------------------------------------------------------------------
|
rlm@10
|
12
|
rlm@10
|
13 (comment
|
rlm@10
|
14 ;; This is a simple function mocking library I accidentally wrote as a side
|
rlm@10
|
15 ;; effect of trying to write an opengl library in clojure. This is loosely
|
rlm@10
|
16 ;; based on various ruby and java mocking frameworks I have used in the past
|
rlm@10
|
17 ;; such as mockito, easymock, and whatever rspec uses.
|
rlm@10
|
18 ;;
|
rlm@10
|
19 ;; expect uses bindings to wrap the functions that are being tested and
|
rlm@10
|
20 ;; then validates the invocation count at the end. The expect macro is the
|
rlm@10
|
21 ;; main entry point and it is given a vector of binding pairs.
|
rlm@10
|
22 ;; The first of each pair names the dependent function you want to override,
|
rlm@10
|
23 ;; while the second is a hashmap containing the mock description, usually
|
rlm@10
|
24 ;; created via the simple helper methods described below.
|
rlm@10
|
25 ;;
|
rlm@10
|
26 ;; Usage:
|
rlm@10
|
27 ;;
|
rlm@10
|
28 ;; there are one or more dependent functions:
|
rlm@10
|
29
|
rlm@10
|
30 (defn dep-fn1 [] "time consuming calculation in 3rd party library")
|
rlm@10
|
31 (defn dep-fn2 [x] "function with undesirable side effects while testing")
|
rlm@10
|
32
|
rlm@10
|
33 ;; then we have the code under test that calls these other functions:
|
rlm@10
|
34
|
rlm@10
|
35 (defn my-code-under-test [] (dep-fn1) (dep-fn2 "a") (+ 2 2))
|
rlm@10
|
36
|
rlm@10
|
37 ;; to test this code, we simply surround it with an expect macro within
|
rlm@10
|
38 ;; the test:
|
rlm@10
|
39
|
rlm@10
|
40 (expect [dep-fn1 (times 1)
|
rlm@10
|
41 dep-fn2 (times 1 (has-args [#(= "a" %)]))]
|
rlm@10
|
42 (my-code-under-test))
|
rlm@10
|
43
|
rlm@10
|
44 ;; When an expectation fails during execution of the function under test,
|
rlm@10
|
45 ;; an error condition function is called with the name of the function
|
rlm@10
|
46 ;; being mocked, the expected form and the actual value. These
|
rlm@10
|
47 ;; error functions can be overridden to allow easy integration into
|
rlm@10
|
48 ;; test frameworks such as test-is by reporting errors in the function
|
rlm@10
|
49 ;; overrides.
|
rlm@10
|
50
|
rlm@10
|
51 ) ;; end comment
|
rlm@10
|
52
|
rlm@10
|
53 (ns clojure.contrib.mock
|
rlm@10
|
54 ^{:author "Matt Clark",
|
rlm@10
|
55 :doc "function mocking/expectations for Clojure" }
|
rlm@10
|
56 (:use [clojure.contrib.seq :only (positions)]
|
rlm@10
|
57 [clojure.contrib.def :only (defmacro-)]))
|
rlm@10
|
58
|
rlm@10
|
59
|
rlm@10
|
60 ;;------------------------------------------------------------------------------
|
rlm@10
|
61 ;; These are the error condition functions. Override them to integrate into
|
rlm@10
|
62 ;; the test framework of your choice, or to simply customize error handling.
|
rlm@10
|
63
|
rlm@10
|
64 (defn report-problem
|
rlm@10
|
65 {:dynamic true}
|
rlm@10
|
66 ([function expected actual]
|
rlm@10
|
67 (report-problem function expected actual "Expectation not met."))
|
rlm@10
|
68 ([function expected actual message]
|
rlm@10
|
69 (prn (str message " Function name: " function
|
rlm@10
|
70 " expected: " expected " actual: " actual))))
|
rlm@10
|
71
|
rlm@10
|
72 (defn no-matching-function-signature
|
rlm@10
|
73 {:dynamic true}
|
rlm@10
|
74 [function expected actual]
|
rlm@10
|
75 (report-problem function expected actual
|
rlm@10
|
76 "No matching real function signature for given argument count."))
|
rlm@10
|
77
|
rlm@10
|
78 (defn unexpected-args
|
rlm@10
|
79 {:dynamic true}
|
rlm@10
|
80 [function expected actual i]
|
rlm@10
|
81 (report-problem function expected actual
|
rlm@10
|
82 (str "Argument " i " has an unexpected value for function.")))
|
rlm@10
|
83
|
rlm@10
|
84 (defn incorrect-invocation-count
|
rlm@10
|
85 {:dynamic true}
|
rlm@10
|
86 [function expected actual]
|
rlm@10
|
87 (report-problem function expected actual "Unexpected invocation count."))
|
rlm@10
|
88
|
rlm@10
|
89
|
rlm@10
|
90 ;;------------------------------------------------------------------------------
|
rlm@10
|
91 ;; Internal Functions - ignore these
|
rlm@10
|
92
|
rlm@10
|
93
|
rlm@10
|
94 (defn- has-arg-count-match?
|
rlm@10
|
95 "Given the sequence of accepted argument vectors for a function,
|
rlm@10
|
96 returns true if at least one matches the given-count value."
|
rlm@10
|
97 [arg-lists given-count]
|
rlm@10
|
98 (some #(let [[ind] (positions #{'&} %)]
|
rlm@10
|
99 (if ind
|
rlm@10
|
100 (>= given-count ind)
|
rlm@10
|
101 (= (count %) given-count)))
|
rlm@10
|
102 arg-lists))
|
rlm@10
|
103
|
rlm@10
|
104
|
rlm@10
|
105 (defn has-matching-signature?
|
rlm@10
|
106 "Calls no-matching-function-signature if no match is found for the given
|
rlm@10
|
107 function. If no argslist meta data is available for the function, it is
|
rlm@10
|
108 not called."
|
rlm@10
|
109 [fn-name args]
|
rlm@10
|
110 (let [arg-count (count args)
|
rlm@10
|
111 arg-lists (:arglists (meta (resolve fn-name)))]
|
rlm@10
|
112 (if (and arg-lists (not (has-arg-count-match? arg-lists arg-count)))
|
rlm@10
|
113 (no-matching-function-signature fn-name arg-lists args))))
|
rlm@10
|
114
|
rlm@10
|
115
|
rlm@10
|
116 (defn make-arg-checker
|
rlm@10
|
117 "Creates the argument verifying function for a replaced dependency within
|
rlm@10
|
118 the expectation bound scope. These functions take the additional argument
|
rlm@10
|
119 of the name of the replaced function, then the rest of their args. It is
|
rlm@10
|
120 designed to be called from the mock function generated in the first argument
|
rlm@10
|
121 of the mock info object created by make-mock."
|
rlm@10
|
122 [arg-preds arg-pred-forms]
|
rlm@10
|
123 (let [sanitized-preds (map (fn [v] (if (fn? v) v #(= v %))) arg-preds)]
|
rlm@10
|
124 (fn [fn-name & args]
|
rlm@10
|
125 (every? true?
|
rlm@10
|
126 (map (fn [pred arg pred-form i] (if (pred arg) true
|
rlm@10
|
127 (unexpected-args fn-name pred-form arg i)))
|
rlm@10
|
128 sanitized-preds args arg-pred-forms (iterate inc 0))))))
|
rlm@10
|
129
|
rlm@10
|
130
|
rlm@10
|
131 (defn make-count-checker
|
rlm@10
|
132 "creates the count checker that is invoked at the end of an expectation, after
|
rlm@10
|
133 the code under test has all been executed. The function returned takes the
|
rlm@10
|
134 name of the associated dependency and the invocation count as arguments."
|
rlm@10
|
135 [pred pred-form]
|
rlm@10
|
136 (let [pred-fn (if (integer? pred) #(= pred %) pred)]
|
rlm@10
|
137 (fn [fn-name v] (if (pred-fn v) true
|
rlm@10
|
138 (incorrect-invocation-count fn-name pred-form v)))))
|
rlm@10
|
139
|
rlm@10
|
140 ; Borrowed from clojure core. Remove if this ever becomes public there.
|
rlm@10
|
141 (defmacro- assert-args
|
rlm@10
|
142 [fnname & pairs]
|
rlm@10
|
143 `(do (when-not ~(first pairs)
|
rlm@10
|
144 (throw (IllegalArgumentException.
|
rlm@10
|
145 ~(str fnname " requires " (second pairs)))))
|
rlm@10
|
146 ~(let [more (nnext pairs)]
|
rlm@10
|
147 (when more
|
rlm@10
|
148 (list* `assert-args fnname more)))))
|
rlm@10
|
149
|
rlm@10
|
150 (defn make-mock
|
rlm@10
|
151 "creates a vector containing the following information for the named function:
|
rlm@10
|
152 1. dependent function replacement - verifies signature, calls arg checker,
|
rlm@10
|
153 increases count, returns return value.
|
rlm@10
|
154 2. an atom containing the invocation count
|
rlm@10
|
155 3. the invocation count checker function
|
rlm@10
|
156 4. a symbol of the name of the function being replaced."
|
rlm@10
|
157 [fn-name expectation-hash]
|
rlm@10
|
158 (assert-args make-mock
|
rlm@10
|
159 (map? expectation-hash) "a map of expectations")
|
rlm@10
|
160 (let [arg-checker (or (expectation-hash :has-args) (fn [& args] true))
|
rlm@10
|
161 count-atom (atom 0)
|
rlm@10
|
162 ret-fn (or
|
rlm@10
|
163 (expectation-hash :calls)
|
rlm@10
|
164 (fn [& args] (expectation-hash :returns)))]
|
rlm@10
|
165 [(fn [& args]
|
rlm@10
|
166 (has-matching-signature? fn-name args)
|
rlm@10
|
167 (apply arg-checker fn-name args)
|
rlm@10
|
168 (swap! count-atom inc)
|
rlm@10
|
169 (apply ret-fn args))
|
rlm@10
|
170 count-atom
|
rlm@10
|
171 (or (expectation-hash :times) (fn [fn-name v] true))
|
rlm@10
|
172 fn-name]))
|
rlm@10
|
173
|
rlm@10
|
174
|
rlm@10
|
175 (defn validate-counts
|
rlm@10
|
176 "given the sequence of all mock data for the expectation, simply calls the
|
rlm@10
|
177 count checker for each dependency."
|
rlm@10
|
178 [mock-data] (doseq [[mfn i checker fn-name] mock-data] (checker fn-name @i)))
|
rlm@10
|
179
|
rlm@10
|
180 (defn ^{:private true} make-bindings [expect-bindings mock-data-sym]
|
rlm@10
|
181 `[~@(interleave (map #(first %) (partition 2 expect-bindings))
|
rlm@10
|
182 (map (fn [i] `(nth (nth ~mock-data-sym ~i) 0))
|
rlm@10
|
183 (range (quot (count expect-bindings) 2))))])
|
rlm@10
|
184
|
rlm@10
|
185
|
rlm@10
|
186 ;;------------------------------------------------------------------------------
|
rlm@10
|
187 ;; These are convenience functions to improve the readability and use of this
|
rlm@10
|
188 ;; library. Useful in expressions such as:
|
rlm@10
|
189 ;; (expect [dep-fn1 (times (more-than 1) (returns 15)) etc)
|
rlm@10
|
190
|
rlm@10
|
191 (defn once [x] (= 1 x))
|
rlm@10
|
192
|
rlm@10
|
193 (defn never [x] (zero? x))
|
rlm@10
|
194
|
rlm@10
|
195 (defn more-than [x] #(< x %))
|
rlm@10
|
196
|
rlm@10
|
197 (defn less-than [x] #(> x %))
|
rlm@10
|
198
|
rlm@10
|
199 (defn between [x y] #(and (< x %) (> y %)))
|
rlm@10
|
200
|
rlm@10
|
201
|
rlm@10
|
202 ;;------------------------------------------------------------------------------
|
rlm@10
|
203 ;; The following functions can be used to build up the expectation hash.
|
rlm@10
|
204
|
rlm@10
|
205 (defn returns
|
rlm@10
|
206 "Creates or associates to an existing expectation hash the :returns key with
|
rlm@10
|
207 a value to be returned by the expectation after a successful invocation
|
rlm@10
|
208 matching its expected arguments (if applicable).
|
rlm@10
|
209 Usage:
|
rlm@10
|
210 (returns ret-value expectation-hash?)"
|
rlm@10
|
211
|
rlm@10
|
212 ([val] (returns val {}))
|
rlm@10
|
213 ([val expectation-hash] (assoc expectation-hash :returns val)))
|
rlm@10
|
214
|
rlm@10
|
215
|
rlm@10
|
216 (defn calls
|
rlm@10
|
217 "Creates or associates to an existing expectation hash the :calls key with a
|
rlm@10
|
218 function that will be called with the given arguments. The return value from
|
rlm@10
|
219 this function will be returned returned by the expected function. If both this
|
rlm@10
|
220 and returns are specified, the return value of \"calls\" will have precedence.
|
rlm@10
|
221 Usage:
|
rlm@10
|
222 (calls some-fn expectation-hash?)"
|
rlm@10
|
223
|
rlm@10
|
224 ([val] (calls val {}))
|
rlm@10
|
225 ([val expectation-hash] (assoc expectation-hash :calls val)))
|
rlm@10
|
226
|
rlm@10
|
227
|
rlm@10
|
228 (defmacro has-args
|
rlm@10
|
229 "Creates or associates to an existing expectation hash the :has-args key with
|
rlm@10
|
230 a value corresponding to a function that will either return true if its
|
rlm@10
|
231 argument expectations are met or throw an exception with the details of the
|
rlm@10
|
232 first failed argument it encounters.
|
rlm@10
|
233 Only specify as many predicates as you are interested in verifying. The rest
|
rlm@10
|
234 of the values are safely ignored.
|
rlm@10
|
235 Usage:
|
rlm@10
|
236 (has-args [arg-pred-1 arg-pred-2 ... arg-pred-n] expectation-hash?)"
|
rlm@10
|
237
|
rlm@10
|
238 ([arg-pred-forms] `(has-args ~arg-pred-forms {}))
|
rlm@10
|
239 ([arg-pred-forms expect-hash-form]
|
rlm@10
|
240 (assert-args has-args
|
rlm@10
|
241 (vector? arg-pred-forms) "a vector of argument predicates")
|
rlm@10
|
242 `(assoc ~expect-hash-form :has-args
|
rlm@10
|
243 (make-arg-checker ~arg-pred-forms '~arg-pred-forms))))
|
rlm@10
|
244
|
rlm@10
|
245
|
rlm@10
|
246 (defmacro times
|
rlm@10
|
247 "Creates or associates to an existing expectation hash the :times key with a
|
rlm@10
|
248 value corresponding to a predicate function which expects an integer value.
|
rlm@10
|
249 This function can either be specified as the first argument to times or can be
|
rlm@10
|
250 the result of calling times with an integer argument, in which case the
|
rlm@10
|
251 predicate will default to being an exact match. This predicate is called at
|
rlm@10
|
252 the end of an expect expression to validate that an expected dependency
|
rlm@10
|
253 function was called the expected number of times.
|
rlm@10
|
254 Usage:
|
rlm@10
|
255 (times n)
|
rlm@10
|
256 (times #(> n %))
|
rlm@10
|
257 (times n expectation-hash)"
|
rlm@10
|
258 ([times-fn] `(times ~times-fn {}))
|
rlm@10
|
259 ([times-fn expectation-hash]
|
rlm@10
|
260 `(assoc ~expectation-hash :times (make-count-checker ~times-fn '~times-fn))))
|
rlm@10
|
261
|
rlm@10
|
262
|
rlm@10
|
263 ;-------------------------------------------------------------------------------
|
rlm@10
|
264 ; The main expect macro.
|
rlm@10
|
265 (defmacro expect
|
rlm@10
|
266 "Use expect to redirect calls to dependent functions that are made within the
|
rlm@10
|
267 code under test. Instead of calling the functions that would normally be used,
|
rlm@10
|
268 temporary stubs are used, which can verify function parameters and call counts.
|
rlm@10
|
269 Return values can also be specified as needed.
|
rlm@10
|
270 Usage:
|
rlm@10
|
271 (expect [dep-fn (has-args [arg-pred1] (times n (returns x)))]
|
rlm@10
|
272 (function-under-test a b c))"
|
rlm@10
|
273
|
rlm@10
|
274 [expect-bindings & body]
|
rlm@10
|
275 (assert-args expect
|
rlm@10
|
276 (vector? expect-bindings) "a vector of expectation bindings"
|
rlm@10
|
277 (even? (count expect-bindings))
|
rlm@10
|
278 "an even number of forms in expectation bindings")
|
rlm@10
|
279 (let [mock-data (gensym "mock-data_")]
|
rlm@10
|
280 `(let [~mock-data (map (fn [args#]
|
rlm@10
|
281 (apply clojure.contrib.mock/make-mock args#))
|
rlm@10
|
282 ~(cons 'list (map (fn [[n m]] (vector (list 'quote n) m))
|
rlm@10
|
283 (partition 2 expect-bindings))))]
|
rlm@10
|
284 (binding ~(make-bindings expect-bindings mock-data) ~@body)
|
rlm@10
|
285 (clojure.contrib.mock/validate-counts ~mock-data) true)))
|