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