view 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
line wrap: on
line source
1 ;;; clojure.contrib.mock.clj: mocking/expectation framework for Clojure
3 ;; by Matt Clark
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 ;;------------------------------------------------------------------------------
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:
30 (defn dep-fn1 [] "time consuming calculation in 3rd party library")
31 (defn dep-fn2 [x] "function with undesirable side effects while testing")
33 ;; then we have the code under test that calls these other functions:
35 (defn my-code-under-test [] (dep-fn1) (dep-fn2 "a") (+ 2 2))
37 ;; to test this code, we simply surround it with an expect macro within
38 ;; the test:
40 (expect [dep-fn1 (times 1)
41 dep-fn2 (times 1 (has-args [#(= "a" %)]))]
42 (my-code-under-test))
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.
51 ) ;; end comment
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-)]))
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.
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))))
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."))
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.")))
84 (defn incorrect-invocation-count
85 {:dynamic true}
86 [function expected actual]
87 (report-problem function expected actual "Unexpected invocation count."))
90 ;;------------------------------------------------------------------------------
91 ;; Internal Functions - ignore these
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))
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))))
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))))))
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)))))
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)))))
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]))
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)))
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))))])
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)
191 (defn once [x] (= 1 x))
193 (defn never [x] (zero? x))
195 (defn more-than [x] #(< x %))
197 (defn less-than [x] #(> x %))
199 (defn between [x y] #(and (< x %) (> y %)))
202 ;;------------------------------------------------------------------------------
203 ;; The following functions can be used to build up the expectation hash.
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?)"
212 ([val] (returns val {}))
213 ([val expectation-hash] (assoc expectation-hash :returns val)))
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?)"
224 ([val] (calls val {}))
225 ([val expectation-hash] (assoc expectation-hash :calls val)))
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?)"
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))))
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))))
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))"
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)))