Mercurial > lasercutter
diff 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 diff
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/clojure/contrib/mock.clj Sat Aug 21 06:25:44 2010 -0400 1.3 @@ -0,0 +1,285 @@ 1.4 +;;; clojure.contrib.mock.clj: mocking/expectation framework for Clojure 1.5 + 1.6 +;; by Matt Clark 1.7 + 1.8 +;; Copyright (c) Matt Clark, 2009. All rights reserved. The use 1.9 +;; and distribution terms for this software are covered by the Eclipse 1.10 +;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php). 1.11 +;; By using this software in any fashion, you are 1.12 +;; agreeing to be bound by the terms of this license. You must not 1.13 +;; remove this notice, or any other, from this software. 1.14 +;;------------------------------------------------------------------------------ 1.15 + 1.16 +(comment 1.17 + ;; This is a simple function mocking library I accidentally wrote as a side 1.18 + ;; effect of trying to write an opengl library in clojure. This is loosely 1.19 + ;; based on various ruby and java mocking frameworks I have used in the past 1.20 + ;; such as mockito, easymock, and whatever rspec uses. 1.21 + ;; 1.22 + ;; expect uses bindings to wrap the functions that are being tested and 1.23 + ;; then validates the invocation count at the end. The expect macro is the 1.24 + ;; main entry point and it is given a vector of binding pairs. 1.25 + ;; The first of each pair names the dependent function you want to override, 1.26 + ;; while the second is a hashmap containing the mock description, usually 1.27 + ;; created via the simple helper methods described below. 1.28 + ;; 1.29 + ;; Usage: 1.30 + ;; 1.31 + ;; there are one or more dependent functions: 1.32 + 1.33 + (defn dep-fn1 [] "time consuming calculation in 3rd party library") 1.34 + (defn dep-fn2 [x] "function with undesirable side effects while testing") 1.35 + 1.36 + ;; then we have the code under test that calls these other functions: 1.37 + 1.38 + (defn my-code-under-test [] (dep-fn1) (dep-fn2 "a") (+ 2 2)) 1.39 + 1.40 + ;; to test this code, we simply surround it with an expect macro within 1.41 + ;; the test: 1.42 + 1.43 + (expect [dep-fn1 (times 1) 1.44 + dep-fn2 (times 1 (has-args [#(= "a" %)]))] 1.45 + (my-code-under-test)) 1.46 + 1.47 + ;; When an expectation fails during execution of the function under test, 1.48 + ;; an error condition function is called with the name of the function 1.49 + ;; being mocked, the expected form and the actual value. These 1.50 + ;; error functions can be overridden to allow easy integration into 1.51 + ;; test frameworks such as test-is by reporting errors in the function 1.52 + ;; overrides. 1.53 + 1.54 + ) ;; end comment 1.55 + 1.56 +(ns clojure.contrib.mock 1.57 + ^{:author "Matt Clark", 1.58 + :doc "function mocking/expectations for Clojure" } 1.59 + (:use [clojure.contrib.seq :only (positions)] 1.60 + [clojure.contrib.def :only (defmacro-)])) 1.61 + 1.62 + 1.63 +;;------------------------------------------------------------------------------ 1.64 +;; These are the error condition functions. Override them to integrate into 1.65 +;; the test framework of your choice, or to simply customize error handling. 1.66 + 1.67 +(defn report-problem 1.68 + {:dynamic true} 1.69 + ([function expected actual] 1.70 + (report-problem function expected actual "Expectation not met.")) 1.71 + ([function expected actual message] 1.72 + (prn (str message " Function name: " function 1.73 + " expected: " expected " actual: " actual)))) 1.74 + 1.75 +(defn no-matching-function-signature 1.76 + {:dynamic true} 1.77 + [function expected actual] 1.78 + (report-problem function expected actual 1.79 + "No matching real function signature for given argument count.")) 1.80 + 1.81 +(defn unexpected-args 1.82 + {:dynamic true} 1.83 + [function expected actual i] 1.84 + (report-problem function expected actual 1.85 + (str "Argument " i " has an unexpected value for function."))) 1.86 + 1.87 +(defn incorrect-invocation-count 1.88 + {:dynamic true} 1.89 + [function expected actual] 1.90 + (report-problem function expected actual "Unexpected invocation count.")) 1.91 + 1.92 + 1.93 +;;------------------------------------------------------------------------------ 1.94 +;; Internal Functions - ignore these 1.95 + 1.96 + 1.97 +(defn- has-arg-count-match? 1.98 + "Given the sequence of accepted argument vectors for a function, 1.99 +returns true if at least one matches the given-count value." 1.100 + [arg-lists given-count] 1.101 + (some #(let [[ind] (positions #{'&} %)] 1.102 + (if ind 1.103 + (>= given-count ind) 1.104 + (= (count %) given-count))) 1.105 + arg-lists)) 1.106 + 1.107 + 1.108 +(defn has-matching-signature? 1.109 + "Calls no-matching-function-signature if no match is found for the given 1.110 +function. If no argslist meta data is available for the function, it is 1.111 +not called." 1.112 + [fn-name args] 1.113 + (let [arg-count (count args) 1.114 + arg-lists (:arglists (meta (resolve fn-name)))] 1.115 + (if (and arg-lists (not (has-arg-count-match? arg-lists arg-count))) 1.116 + (no-matching-function-signature fn-name arg-lists args)))) 1.117 + 1.118 + 1.119 +(defn make-arg-checker 1.120 + "Creates the argument verifying function for a replaced dependency within 1.121 +the expectation bound scope. These functions take the additional argument 1.122 +of the name of the replaced function, then the rest of their args. It is 1.123 +designed to be called from the mock function generated in the first argument 1.124 +of the mock info object created by make-mock." 1.125 + [arg-preds arg-pred-forms] 1.126 + (let [sanitized-preds (map (fn [v] (if (fn? v) v #(= v %))) arg-preds)] 1.127 + (fn [fn-name & args] 1.128 + (every? true? 1.129 + (map (fn [pred arg pred-form i] (if (pred arg) true 1.130 + (unexpected-args fn-name pred-form arg i))) 1.131 + sanitized-preds args arg-pred-forms (iterate inc 0)))))) 1.132 + 1.133 + 1.134 +(defn make-count-checker 1.135 + "creates the count checker that is invoked at the end of an expectation, after 1.136 +the code under test has all been executed. The function returned takes the 1.137 +name of the associated dependency and the invocation count as arguments." 1.138 + [pred pred-form] 1.139 + (let [pred-fn (if (integer? pred) #(= pred %) pred)] 1.140 + (fn [fn-name v] (if (pred-fn v) true 1.141 + (incorrect-invocation-count fn-name pred-form v))))) 1.142 + 1.143 +; Borrowed from clojure core. Remove if this ever becomes public there. 1.144 +(defmacro- assert-args 1.145 + [fnname & pairs] 1.146 + `(do (when-not ~(first pairs) 1.147 + (throw (IllegalArgumentException. 1.148 + ~(str fnname " requires " (second pairs))))) 1.149 + ~(let [more (nnext pairs)] 1.150 + (when more 1.151 + (list* `assert-args fnname more))))) 1.152 + 1.153 +(defn make-mock 1.154 + "creates a vector containing the following information for the named function: 1.155 +1. dependent function replacement - verifies signature, calls arg checker, 1.156 +increases count, returns return value. 1.157 +2. an atom containing the invocation count 1.158 +3. the invocation count checker function 1.159 +4. a symbol of the name of the function being replaced." 1.160 + [fn-name expectation-hash] 1.161 + (assert-args make-mock 1.162 + (map? expectation-hash) "a map of expectations") 1.163 + (let [arg-checker (or (expectation-hash :has-args) (fn [& args] true)) 1.164 + count-atom (atom 0) 1.165 + ret-fn (or 1.166 + (expectation-hash :calls) 1.167 + (fn [& args] (expectation-hash :returns)))] 1.168 + [(fn [& args] 1.169 + (has-matching-signature? fn-name args) 1.170 + (apply arg-checker fn-name args) 1.171 + (swap! count-atom inc) 1.172 + (apply ret-fn args)) 1.173 + count-atom 1.174 + (or (expectation-hash :times) (fn [fn-name v] true)) 1.175 + fn-name])) 1.176 + 1.177 + 1.178 +(defn validate-counts 1.179 + "given the sequence of all mock data for the expectation, simply calls the 1.180 +count checker for each dependency." 1.181 + [mock-data] (doseq [[mfn i checker fn-name] mock-data] (checker fn-name @i))) 1.182 + 1.183 +(defn ^{:private true} make-bindings [expect-bindings mock-data-sym] 1.184 + `[~@(interleave (map #(first %) (partition 2 expect-bindings)) 1.185 + (map (fn [i] `(nth (nth ~mock-data-sym ~i) 0)) 1.186 + (range (quot (count expect-bindings) 2))))]) 1.187 + 1.188 + 1.189 +;;------------------------------------------------------------------------------ 1.190 +;; These are convenience functions to improve the readability and use of this 1.191 +;; library. Useful in expressions such as: 1.192 +;; (expect [dep-fn1 (times (more-than 1) (returns 15)) etc) 1.193 + 1.194 +(defn once [x] (= 1 x)) 1.195 + 1.196 +(defn never [x] (zero? x)) 1.197 + 1.198 +(defn more-than [x] #(< x %)) 1.199 + 1.200 +(defn less-than [x] #(> x %)) 1.201 + 1.202 +(defn between [x y] #(and (< x %) (> y %))) 1.203 + 1.204 + 1.205 +;;------------------------------------------------------------------------------ 1.206 +;; The following functions can be used to build up the expectation hash. 1.207 + 1.208 +(defn returns 1.209 + "Creates or associates to an existing expectation hash the :returns key with 1.210 +a value to be returned by the expectation after a successful invocation 1.211 +matching its expected arguments (if applicable). 1.212 +Usage: 1.213 +(returns ret-value expectation-hash?)" 1.214 + 1.215 + ([val] (returns val {})) 1.216 + ([val expectation-hash] (assoc expectation-hash :returns val))) 1.217 + 1.218 + 1.219 +(defn calls 1.220 + "Creates or associates to an existing expectation hash the :calls key with a 1.221 +function that will be called with the given arguments. The return value from 1.222 +this function will be returned returned by the expected function. If both this 1.223 +and returns are specified, the return value of \"calls\" will have precedence. 1.224 +Usage: 1.225 +(calls some-fn expectation-hash?)" 1.226 + 1.227 + ([val] (calls val {})) 1.228 + ([val expectation-hash] (assoc expectation-hash :calls val))) 1.229 + 1.230 + 1.231 +(defmacro has-args 1.232 + "Creates or associates to an existing expectation hash the :has-args key with 1.233 +a value corresponding to a function that will either return true if its 1.234 +argument expectations are met or throw an exception with the details of the 1.235 +first failed argument it encounters. 1.236 +Only specify as many predicates as you are interested in verifying. The rest 1.237 +of the values are safely ignored. 1.238 +Usage: 1.239 +(has-args [arg-pred-1 arg-pred-2 ... arg-pred-n] expectation-hash?)" 1.240 + 1.241 + ([arg-pred-forms] `(has-args ~arg-pred-forms {})) 1.242 + ([arg-pred-forms expect-hash-form] 1.243 + (assert-args has-args 1.244 + (vector? arg-pred-forms) "a vector of argument predicates") 1.245 + `(assoc ~expect-hash-form :has-args 1.246 + (make-arg-checker ~arg-pred-forms '~arg-pred-forms)))) 1.247 + 1.248 + 1.249 +(defmacro times 1.250 + "Creates or associates to an existing expectation hash the :times key with a 1.251 +value corresponding to a predicate function which expects an integer value. 1.252 +This function can either be specified as the first argument to times or can be 1.253 +the result of calling times with an integer argument, in which case the 1.254 +predicate will default to being an exact match. This predicate is called at 1.255 +the end of an expect expression to validate that an expected dependency 1.256 +function was called the expected number of times. 1.257 +Usage: 1.258 +(times n) 1.259 +(times #(> n %)) 1.260 +(times n expectation-hash)" 1.261 + ([times-fn] `(times ~times-fn {})) 1.262 + ([times-fn expectation-hash] 1.263 + `(assoc ~expectation-hash :times (make-count-checker ~times-fn '~times-fn)))) 1.264 + 1.265 + 1.266 +;------------------------------------------------------------------------------- 1.267 +; The main expect macro. 1.268 +(defmacro expect 1.269 + "Use expect to redirect calls to dependent functions that are made within the 1.270 +code under test. Instead of calling the functions that would normally be used, 1.271 +temporary stubs are used, which can verify function parameters and call counts. 1.272 +Return values can also be specified as needed. 1.273 +Usage: 1.274 +(expect [dep-fn (has-args [arg-pred1] (times n (returns x)))] 1.275 + (function-under-test a b c))" 1.276 + 1.277 + [expect-bindings & body] 1.278 + (assert-args expect 1.279 + (vector? expect-bindings) "a vector of expectation bindings" 1.280 + (even? (count expect-bindings)) 1.281 + "an even number of forms in expectation bindings") 1.282 + (let [mock-data (gensym "mock-data_")] 1.283 + `(let [~mock-data (map (fn [args#] 1.284 + (apply clojure.contrib.mock/make-mock args#)) 1.285 + ~(cons 'list (map (fn [[n m]] (vector (list 'quote n) m)) 1.286 + (partition 2 expect-bindings))))] 1.287 + (binding ~(make-bindings expect-bindings mock-data) ~@body) 1.288 + (clojure.contrib.mock/validate-counts ~mock-data) true)))