annotate 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
rev   line source
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)))