Mercurial > lasercutter
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 Clojure3 ;; by Matt Clark5 ;; Copyright (c) Matt Clark, 2009. All rights reserved. The use6 ;; and distribution terms for this software are covered by the Eclipse7 ;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php).8 ;; By using this software in any fashion, you are9 ;; agreeing to be bound by the terms of this license. You must not10 ;; remove this notice, or any other, from this software.11 ;;------------------------------------------------------------------------------13 (comment14 ;; This is a simple function mocking library I accidentally wrote as a side15 ;; effect of trying to write an opengl library in clojure. This is loosely16 ;; based on various ruby and java mocking frameworks I have used in the past17 ;; such as mockito, easymock, and whatever rspec uses.18 ;;19 ;; expect uses bindings to wrap the functions that are being tested and20 ;; then validates the invocation count at the end. The expect macro is the21 ;; 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, usually24 ;; 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 within38 ;; 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 function46 ;; being mocked, the expected form and the actual value. These47 ;; error functions can be overridden to allow easy integration into48 ;; test frameworks such as test-is by reporting errors in the function49 ;; overrides.51 ) ;; end comment53 (ns clojure.contrib.mock54 ^{: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 into62 ;; the test framework of your choice, or to simply customize error handling.64 (defn report-problem65 {: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: " function70 " expected: " expected " actual: " actual))))72 (defn no-matching-function-signature73 {:dynamic true}74 [function expected actual]75 (report-problem function expected actual76 "No matching real function signature for given argument count."))78 (defn unexpected-args79 {:dynamic true}80 [function expected actual i]81 (report-problem function expected actual82 (str "Argument " i " has an unexpected value for function.")))84 (defn incorrect-invocation-count85 {:dynamic true}86 [function expected actual]87 (report-problem function expected actual "Unexpected invocation count."))90 ;;------------------------------------------------------------------------------91 ;; Internal Functions - ignore these94 (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 ind100 (>= 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 given107 function. If no argslist meta data is available for the function, it is108 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-checker117 "Creates the argument verifying function for a replaced dependency within118 the expectation bound scope. These functions take the additional argument119 of the name of the replaced function, then the rest of their args. It is120 designed to be called from the mock function generated in the first argument121 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) true127 (unexpected-args fn-name pred-form arg i)))128 sanitized-preds args arg-pred-forms (iterate inc 0))))))131 (defn make-count-checker132 "creates the count checker that is invoked at the end of an expectation, after133 the code under test has all been executed. The function returned takes the134 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) true138 (incorrect-invocation-count fn-name pred-form v)))))140 ; Borrowed from clojure core. Remove if this ever becomes public there.141 (defmacro- assert-args142 [fnname & pairs]143 `(do (when-not ~(first pairs)144 (throw (IllegalArgumentException.145 ~(str fnname " requires " (second pairs)))))146 ~(let [more (nnext pairs)]147 (when more148 (list* `assert-args fnname more)))))150 (defn make-mock151 "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 count155 3. the invocation count checker function156 4. a symbol of the name of the function being replaced."157 [fn-name expectation-hash]158 (assert-args make-mock159 (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 (or163 (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-atom171 (or (expectation-hash :times) (fn [fn-name v] true))172 fn-name]))175 (defn validate-counts176 "given the sequence of all mock data for the expectation, simply calls the177 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 this188 ;; 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 returns206 "Creates or associates to an existing expectation hash the :returns key with207 a value to be returned by the expectation after a successful invocation208 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 calls217 "Creates or associates to an existing expectation hash the :calls key with a218 function that will be called with the given arguments. The return value from219 this function will be returned returned by the expected function. If both this220 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-args229 "Creates or associates to an existing expectation hash the :has-args key with230 a value corresponding to a function that will either return true if its231 argument expectations are met or throw an exception with the details of the232 first failed argument it encounters.233 Only specify as many predicates as you are interested in verifying. The rest234 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-args241 (vector? arg-pred-forms) "a vector of argument predicates")242 `(assoc ~expect-hash-form :has-args243 (make-arg-checker ~arg-pred-forms '~arg-pred-forms))))246 (defmacro times247 "Creates or associates to an existing expectation hash the :times key with a248 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 be250 the result of calling times with an integer argument, in which case the251 predicate will default to being an exact match. This predicate is called at252 the end of an expect expression to validate that an expected dependency253 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 expect266 "Use expect to redirect calls to dependent functions that are made within the267 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 expect276 (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)))