├── run-tests.sh ├── src └── conjure │ ├── ns_with_macro.clj │ └── core.clj ├── test └── conjure │ ├── macro_wrapping_conjure_test.clj │ └── core_test.clj ├── .gitignore ├── project.clj └── README.md /run-tests.sh: -------------------------------------------------------------------------------- 1 | lein multi test --with "1.2.0" ; 2 | lein multi test --with "1.3.0" ; 3 | lein multi test --with "1.4.0" ; 4 | lein multi test --with "1.5.0" 5 | -------------------------------------------------------------------------------- /src/conjure/ns_with_macro.clj: -------------------------------------------------------------------------------- 1 | (ns conjure.ns-with-macro 2 | (:use conjure.core)) 3 | 4 | (defmacro with-nil?-always-foo [& body] 5 | `(stubbing [nil? :foo] 6 | ~@body)) 7 | 8 | (defmacro with-mocked-nil? [& body] 9 | `(mocking [nil?] 10 | ~@body)) 11 | -------------------------------------------------------------------------------- /test/conjure/macro_wrapping_conjure_test.clj: -------------------------------------------------------------------------------- 1 | (ns conjure.macro_wrapping-conjure-test 2 | (:use conjure.ns-with-macro)) 3 | 4 | (defn foo [] 5 | (with-nil?-always-foo 6 | :if-this-code-compiles-we-can-successfully-create-a-macro)) 7 | 8 | (defn bar [] 9 | (with-mocked-nil? 10 | :if-this-code-compiles-we-can-successfully-create-a-macro)) 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *#* 3 | lib/* 4 | config/insta.conf 5 | db/schema.rb 6 | classes/* 7 | fe/classes/* 8 | api/classes/* 9 | .clojure.conf 10 | .classpath 11 | .project 12 | .lein-deps-sum 13 | .lein-plugins 14 | config/config.clj 15 | scripts/furtive 16 | logs/* 17 | log/* 18 | .DS_Store 19 | *flymake.js 20 | all.js 21 | furtive.iml 22 | .idea 23 | .lein-failures 24 | api/lib/* 25 | /api/.lein-failures 26 | /api/pom.xml 27 | /api/config/zolo.clj 28 | conjure.iml 29 | pom.xml 30 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (def common-deps '[]) 2 | 3 | (defproject org.clojars.runa/conjure "2.2.0" 4 | :description "Conjure mocking and stubbing" 5 | :dependencies ~(cons '[org.clojure/clojure "1.3.0"] 6 | common-deps) 7 | :dev-dependencies [[jonase/kibit "0.0.3"] 8 | [jonase/eastwood "0.0.2"] 9 | [lein-multi "1.1.0"]] 10 | :plugins [[lein-swank "1.4.4"] 11 | [lein-difftest "1.3.8"]] 12 | :multi-deps {"1.2.0" [[org.clojure/clojure "1.2.0"]] 13 | "1.2.1" [[org.clojure/clojure "1.2.1"]] 14 | "1.3.0" [[org.clojure/clojure "1.3.0"]] 15 | "1.4.0" [[org.clojure/clojure "1.4.0"]] 16 | "1.5.0" [[org.clojure/clojure "1.5.0-alpha3"]] 17 | :all ~common-deps}) 18 | 19 | 20 | (def common-deps '[[org.clojure/tools.logging "0.2.3"]]) 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Conjure 2 | ======= 3 | 4 | Simple mocking and stubbing for Clojure unit-tests. Supports Clojure 1.2 through 1.5. 5 | 6 | Usage 7 | ===== 8 | 9 | ```clj 10 | [org.clojars.runa/conjure "2.1.3"] 11 | 12 | (:require [conjure.core]) 13 | ``` 14 | 15 | The Set up 16 | ========== 17 | 18 | Imagine we had the following functions - 19 | 20 | ```clj 21 | (defn xx [a b] 22 | 10) 23 | 24 | (defn yy [z] 25 | 20) 26 | 27 | (defn fn-under-test [] 28 | (xx 1 2) 29 | (yy "blah")) 30 | 31 | (defn another-fn-under-test [] 32 | (+ (xx nil nil) (yy nil))) 33 | ``` 34 | 35 | Also imagine that we had to test fn-under-test and another-fn-under-test, and we didn’t want to have to deal with the xx or yy functions. Maybe they’re horrible functions that open connections to computers running Windoze or something, I dunno. 36 | 37 | Mocking 38 | ======= 39 | 40 | Here’s how we might mock them out - 41 | 42 | ```clj 43 | (deftest test-basic-mocking 44 | (mocking [xx yy] 45 | (fn-under-test) 46 | (verify-call-times-for xx 1) 47 | (verify-call-times-for yy 1) 48 | (verify-first-call-args-for xx 1 2) 49 | (verify-first-call-args-for yy "blah"))) 50 | ``` 51 | 52 | Pretty straightforward, eh? You just use the `mocking` macro, specifying all the functions that need to be mocked out. Then, within the scope of `mocking`, you call your functions that need to be tested. The calls to the specified functions will get mocked out (they won’t occur), and you can then use things like verify-call-times-for and verify-first-call-args-for to ensure things worked as expected. 53 | 54 | Stubbing 55 | ======== 56 | 57 | Sometimes your tests need to specify values to be returned by the functions being mocked out. That’s where `stubbing` comes in. 58 | 59 | Here’s how it works - 60 | 61 | ```clj 62 | (deftest test-basic-stubbing 63 | (is (= (another-fn-under-test) 30)) 64 | (stubbing [xx 1 yy 2] 65 | (is (= (another-fn-under-test) 3)))) 66 | ``` 67 | 68 | So that’s it! Pretty simple. Note how within the scope of `stubbing`, `xx` returns `1` and `yy` returns `2`. Now, for the implementation. 69 | 70 | Instrumenting 71 | ============= 72 | 73 | Sometimes you just want to inspect the calls of some function without otherwise interfering with its execution. 74 | 75 | ```clj 76 | (defn my-inc [n] 77 | (inc n)) ;; inc has :inline metadata so cannot be faked 78 | 79 | (deftest test-instumenting 80 | (instrumenting [my-inc] 81 | (is (= 43 (my-inc 42))) 82 | (verify-called-once-with-args my-inc 42))) 83 | ``` 84 | 85 | License 86 | ======= 87 | Conjure uses the MIT License (MIT) - http://opensource.org/licenses/MIT -------------------------------------------------------------------------------- /test/conjure/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns conjure.core-test 2 | (:use [clojure.test :only [run-tests deftest is are]] 3 | conjure.core)) 4 | 5 | 6 | (deftest test-all-public-api-fns-have-docstrings 7 | (is (= [] 8 | (->> (ns-publics 'conjure.core) 9 | vals 10 | (remove (comp :doc meta)))))) 11 | 12 | (defn xx [a b] 13 | 10) 14 | 15 | (defn yy [z] 16 | 20) 17 | 18 | (defn fn-under-test [] 19 | (xx 1 2) 20 | (yy "blah")) 21 | 22 | (defn another-fn-under-test [& _args] 23 | (+ (xx nil nil) 24 | (yy nil))) 25 | 26 | (deftest test-basic-mocking 27 | (mocking [xx yy] 28 | (verify-call-times-for xx 0) 29 | (verify-call-times-for yy 0) 30 | (fn-under-test) 31 | (verify-call-times-for xx 1) 32 | (verify-call-times-for yy 1) 33 | (verify-first-call-args-for xx 1 2) 34 | (verify-first-call-args-for yy "blah") 35 | 36 | ;; common case: combines fact that it had one call and its args were correct 37 | (verify-called-once-with-args xx 1 2) 38 | (verify-called-once-with-args yy "blah"))) 39 | 40 | (deftest test-basic-stubbing 41 | (is (= (another-fn-under-test) 30)) 42 | (stubbing [xx 1 yy 2] 43 | (is (= (another-fn-under-test) 3)) 44 | (verify-called-once-with-args xx nil nil) 45 | (verify-called-once-with-args yy nil))) 46 | 47 | (deftest test-fn-based-stubs 48 | (is (= (another-fn-under-test) 30)) 49 | (stubbing [xx 1 yy (fn [_] (+ 2 3))] 50 | (is (= (another-fn-under-test) 6))) 51 | (stubbing [xx 1 yy (fn [_] (+ 2 (xx :a :b )))] 52 | (is (= (another-fn-under-test) 4)))) 53 | 54 | (defn three-arg-fn [a b c] 55 | "Bonjour!") 56 | 57 | (deftest test-verify-first-call-args-for-indices 58 | (mocking [three-arg-fn] 59 | (three-arg-fn "one" "two" "three") 60 | (verify-first-call-args-for-indices three-arg-fn [0 2] "one" "three"))) 61 | 62 | (deftest test-verify-nth-call-args-for-indices 63 | (mocking [three-arg-fn] 64 | (three-arg-fn "one" "two" "three") 65 | (three-arg-fn "four" "five" "six") 66 | (verify-nth-call-args-for-indices 0 three-arg-fn [0 2] "one" "three") 67 | (verify-nth-call-args-for-indices 1 three-arg-fn [1 2] "five" "six"))) 68 | 69 | (defn f [] 70 | "called f") 71 | 72 | (deftest test-passes-args-through-to-fake-fn 73 | (stubbing [f (fn [& msgs] msgs)] 74 | (is (= ["a" "b" "c"] (f "a" "b" "c"))))) 75 | 76 | (deftest test-can-stub-multimethods 77 | (defmulti mm :shape) 78 | (defmethod mm :triangle [x] 79 | "called triangle multimethod") 80 | (stubbing [f mm] 81 | (is (= "called triangle multimethod" (f {:shape :triangle}))))) 82 | 83 | (defn a [] "a") 84 | 85 | (deftest test-stub-fn-with-return-vals 86 | (stubbing [a (stub-fn-with-return-vals "b" "c" "d")] 87 | (is (= "b" (a))) 88 | (is (= "c" (a))) 89 | (is (= "d" (a))) 90 | (is (= "d" (a))) 91 | (is (= "d" (a)))) 92 | 93 | (is (thrown-with-msg? 94 | RuntimeException 95 | #"Looks like you may have forgotten to specify return values" 96 | (stub-fn-with-return-vals)))) 97 | 98 | (defn my-inc [n] 99 | (inc n)) ;; inc has :inline metadata so cannot be faked 100 | 101 | (deftest test-instumenting 102 | (instrumenting [my-inc] 103 | (is (= 43 (my-inc 42))) 104 | (verify-called-once-with-args my-inc 42))) 105 | 106 | (deftest test-verifies-only-called-from-within-fake-contexts 107 | (is (thrown-with-msg? 108 | AssertionError 109 | #"cannot be called outside" 110 | (verify-call-times-for my-inc 2))) 111 | 112 | (is (thrown-with-msg? 113 | AssertionError 114 | #"cannot be called outside" 115 | (verify-first-call-args-for my-inc 2))) 116 | 117 | (is (thrown-with-msg? 118 | AssertionError 119 | #"cannot be called outside" 120 | (verify-called-once-with-args my-inc 2))) 121 | 122 | (is (thrown-with-msg? 123 | AssertionError 124 | #"cannot be called outside" 125 | (verify-nth-call-args-for 0 my-inc 2))) 126 | 127 | (is (thrown-with-msg? 128 | AssertionError 129 | #"cannot be called outside" 130 | (verify-first-call-args-for-indices my-inc [0] 2)))) 131 | 132 | (deftest test-verifies-only-called-on-conjurified-fns 133 | (mocking [] 134 | (is (thrown-with-msg? 135 | AssertionError 136 | #"was called on a function that was not specified" 137 | (verify-call-times-for my-inc 2)))) 138 | 139 | (mocking [] 140 | (is (thrown-with-msg? 141 | AssertionError 142 | #"was called on a function that was not specified" 143 | (verify-first-call-args-for my-inc 2)))) 144 | 145 | (mocking [] 146 | (is (thrown-with-msg? 147 | AssertionError 148 | #"was called on a function that was not specified" 149 | (verify-called-once-with-args my-inc 2)))) 150 | 151 | (mocking [] 152 | (is (thrown-with-msg? 153 | AssertionError 154 | #"was called on a function that was not specified" 155 | (verify-nth-call-args-for 0 my-inc 2)))) 156 | 157 | (mocking [] 158 | (is (thrown-with-msg? 159 | AssertionError 160 | #"was called on a function that was not specified" 161 | (verify-first-call-args-for-indices my-inc [0] 2))))) 162 | 163 | 164 | (deftest test-dissallows-nesting 165 | (mocking [inc] 166 | (is (thrown-with-msg? 167 | AssertionError 168 | #"cannot be called from within" 169 | (stubbing [str "a"])))) 170 | (stubbing [inc 3] 171 | (is (thrown-with-msg? 172 | AssertionError 173 | #"cannot be called from within" 174 | (instrumenting [str])))) 175 | (instrumenting [inc] 176 | (is (thrown-with-msg? 177 | AssertionError 178 | #"cannot be called from within" 179 | (mocking [str]))))) 180 | 181 | -------------------------------------------------------------------------------- /src/conjure/core.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:doc "Simple mocking and stubbing for Clojure unit-tests. Supports Clojure 1.2 through 1.5."} 2 | conjure.core 3 | (:use clojure.test 4 | [clojure.string :only [join]])) 5 | 6 | (def ^{:doc "Atom holding a map of faked function names to the arglist that they 7 | were called with. This is used internally by Conjure."} 8 | call-times (atom {})) 9 | 10 | (def ^{:doc "Has a value of true when inside one of the Conjure faking macros. 11 | Used to give users helpful error messages." 12 | :dynamic true} 13 | *in-fake-context* false) 14 | 15 | (defn- return-value-for-stub-fn [return-value args] 16 | (if (or (fn? return-value) 17 | (instance? clojure.lang.MultiFn return-value)) 18 | (apply return-value args) 19 | return-value)) 20 | 21 | (defn stub-fn 22 | "Wraps a function, instrumenting it to record information about when it was 23 | called, and to return the return-value specified. The actual function is never called. 24 | This is used internally by Conjure." 25 | [f return-value] 26 | (let [stubbed-f (fn this [& args] 27 | (swap! call-times update-in [this] conj (vec args)) 28 | (return-value-for-stub-fn return-value (vec args)))] 29 | (swap! call-times assoc stubbed-f []) 30 | stubbed-f)) 31 | 32 | (defn instrumented-fn 33 | "Wraps a function, instrumenting it to record information about when it was 34 | called. The actual function is still called. This is used internally by Conjure." 35 | [f] 36 | (let [instrumented-f (fn this [& args] 37 | (swap! call-times update-in [this] conj (vec args)) 38 | (apply f args))] 39 | (swap! call-times assoc instrumented-f []) 40 | instrumented-f)) 41 | 42 | (defn stub-fn-with-return-vals 43 | "Creates a anonymous function that, on each successive call, returns the next 44 | item in the supplied return values. When it runs out of values to return, will 45 | continually return the rightmost supplied return value. Meant to be used on 46 | the righthand side of a stubbing macro's binding." 47 | [& return-vals] 48 | (when (empty? return-vals) 49 | (throw (RuntimeException. "Looks like you may have forgotten to specify return values."))) 50 | (let [return-val-atom (atom return-vals)] 51 | (fn [& _] 52 | (let [current-result (first @return-val-atom)] 53 | (when (> (count @return-val-atom) 1) 54 | (swap! return-val-atom rest)) 55 | current-result)))) 56 | 57 | (defn mock-fn 58 | "Wraps a function, instrumenting it to record information about when it was 59 | called, and stubbing it to return nil. This is used internally by Conjure." 60 | [function-name] 61 | (stub-fn function-name nil)) 62 | 63 | (defn assert-not-in-fake-context 64 | "Used internally by Conjure to make sure mocking/stubbing/instrumenting are 65 | not being nested, because they do not work as you might expect when nested." 66 | [macro-name] 67 | (when *in-fake-context* 68 | (throw (AssertionError. (str "Conjure macro " macro-name " cannot be called from within" 69 | " another of `conjure.core/mocking`, `conjure.core/stubbing`, or `conjure.core/instrumenting`"))))) 70 | 71 | (defn assert-in-fake-context 72 | "Used internally by Conjure to make sure `verify-x` macros ae only called from with Conjure's fakes" 73 | [macro-name] 74 | (when-not *in-fake-context* 75 | (throw (AssertionError. (str "Conjure macro " macro-name " cannot be called outside " 76 | "of one of `conjure.core/mocking`, `conjure.core/stubbing`, or `conjure.core/instrumenting`"))))) 77 | 78 | (defn assert-conjurified-fn 79 | "Used internally by Conjure to make sure verify-x macros are only called on 80 | functions that have been used in `mocking`, `stubbing`, or `instrumenting`" 81 | [macro-name fn-name] 82 | (when-not (contains? @call-times fn-name) 83 | (throw (AssertionError. (str "Conjure macro " macro-name " was called on a function that was not " 84 | "specified in one of `conjure.core/mocking`, `conjure.core/stubbing`, " 85 | "or `conjure.core/instrumenting`"))))) 86 | 87 | (defmacro verify-call-times-for 88 | "Asserts that the faked function was called n times" 89 | [fn-name n] 90 | `(do 91 | (assert-in-fake-context "verify-call-times-for") 92 | (assert-conjurified-fn "verify-call-times-for" ~fn-name) 93 | (is (= ~n (count (get @call-times ~fn-name))) 94 | (str "(verify-call-times-for " ~fn-name " " ~n ")")))) 95 | 96 | (defmacro verify-first-call-args-for 97 | "Asserts that the faked function was called at least once, and the first call 98 | was passed the args specified" 99 | [fn-name & args] 100 | `(do 101 | (assert-in-fake-context "verify-first-call-args-for") 102 | (assert-conjurified-fn "verify-first-call-args-for" ~fn-name) 103 | (is (= true (pos? (count (get @call-times ~fn-name)))) 104 | (str "(verify-first-call-args-for " ~fn-name " " ~(join " " args) ")")) 105 | (is (= ~(vec args) (first (get @call-times ~fn-name))) 106 | (str "(verify-first-call-args-for " ~fn-name " " ~(join " " args) ")")))) 107 | 108 | (defmacro verify-called-once-with-args 109 | "Asserts that the faked function was called exactly once, and was passed the 110 | args specified" 111 | [fn-name & args] 112 | `(do 113 | (assert-in-fake-context "verify-called-once-with-args") 114 | (assert-conjurified-fn "verify-called-once-with-args" ~fn-name) 115 | (conjure.core/verify-call-times-for ~fn-name 1) 116 | (conjure.core/verify-first-call-args-for ~fn-name ~@args))) 117 | 118 | (defmacro verify-nth-call-args-for 119 | "Asserts that the function was called n times, and the nth time was passed the 120 | args specified" 121 | [n fn-name & args] 122 | `(do 123 | (assert-in-fake-context "verify-nth-call-args-for") 124 | (assert-conjurified-fn "verify-nth-call-args-for" ~fn-name) 125 | (is (= ~(vec args) (nth (get @call-times ~fn-name) ~(dec n))) 126 | (str "(verify-nth-call-args-for " ~n " " ~fn-name " " ~(join " " args) ")")))) 127 | 128 | (defmacro verify-nth-call-args-for-indices 129 | "Asserts that the function was called at least once, and the nth call was 130 | passed the args specified, into the indices of the arglist specified. In 131 | other words, it checks only the particular args you care about." 132 | [n fn-name indices & args] 133 | 134 | `(do 135 | (assert-in-fake-context "verify-first-call-args-for-indices") 136 | (assert-conjurified-fn "verify-first-call-args-for-indices" ~fn-name) 137 | (is (< ~n (count (get @call-times ~fn-name))) 138 | (str "(verify-nth-call-args-for-indices " ~n " " ~fn-name " " ~indices " " ~(join " " args) ")")) 139 | (let [nth-call-args# (nth (get @call-times ~fn-name) ~n) 140 | indices-in-range?# (< (apply max ~indices) (count nth-call-args#))] 141 | (if indices-in-range?# 142 | (is (= ~(vec args) (map #(nth nth-call-args# %) ~indices)) 143 | (str "(verify-first-call-args-for-indices " ~n " " ~fn-name " " ~indices " " ~(join " " args) ")")) 144 | (is (= :fail (format "indices %s are out of range for the args, %s" ~indices ~(vec args))) 145 | (str "(verify-first-call-args-for-indices " ~n " " ~fn-name " " ~indices " " ~(join " " args) ")")))))) 146 | 147 | 148 | (defmacro verify-first-call-args-for-indices 149 | "Asserts that the function was called at least once, and the first call was 150 | passed the args specified, into the indices of the arglist specified. In 151 | other words, it checks only the particular args you care about." 152 | [fn-name indices & args] 153 | `(conjure.core/verify-nth-call-args-for-indices 0 ~fn-name ~indices ~@args)) 154 | 155 | (def ^{:private true :const true} binding-or-with-redefs (if (= 2 (:minor *clojure-version*)) 156 | 'binding 157 | 'with-redefs)) 158 | 159 | (defn- with-installed-fakes [macro-name fn-names fake-fns body] 160 | `(try 161 | (assert-not-in-fake-context ~macro-name) 162 | (binding [*in-fake-context* true] 163 | (~binding-or-with-redefs [~@(interleave fn-names fake-fns)] 164 | ~@body)) 165 | (finally 166 | (reset! call-times {})))) 167 | 168 | (defmacro mocking 169 | "Within the body of this macro you may use the various conjure verify-* macros 170 | to make assertions about how the mocked functions have been called. The original 171 | function is not actually called, instead a a return value of nil is produced and 172 | nothing is executed." 173 | [fn-names & body] 174 | (let [mocks (for [name fn-names] 175 | `(conjure.core/mock-fn ~name))] 176 | (with-installed-fakes "mocking" fn-names mocks body))) 177 | 178 | (defmacro instrumenting 179 | "Within the body of this macro you may use the various conjure verify-* macros 180 | to make assertions about how the mocked functions have been called. The original 181 | function is still called." 182 | [fn-names & body] 183 | (let [instrumented-fns (for [name fn-names] 184 | `(conjure.core/instrumented-fn ~name))] 185 | (with-installed-fakes "instrumenting" fn-names instrumented-fns body))) 186 | 187 | (defmacro stubbing 188 | "Within the body of this macro you may use the various conjure verify-* macros 189 | to make assertions about how the mocked functions have been called. This is 190 | like mocking, except you also specify the return value of the faked functions. 191 | If the return value is specified as a function, then that function will be 192 | used as the stub (it won't return the function - instead the function will be 193 | used in pace of the orginal." 194 | [stub-forms & body] 195 | (let [fn-names (take-nth 2 stub-forms) 196 | stubs (for [[fn-name return-value] (partition 2 stub-forms)] 197 | `(conjure.core/stub-fn ~fn-name ~return-value))] 198 | (with-installed-fakes "stubbing" fn-names stubs body))) 199 | --------------------------------------------------------------------------------