Mockery is a simple and lightweight library to mock Clojure functions. It is inspired by Python's mock tool initially written by Michael Foord.
Installation
Add it to your dependencies:
[mockery "0.1.4"]
Mockery is used mostly for unit tests so it's better to put it into the :test
project's profile but not the global one.
Why?
Imagine you have a function that fetches data from any remote service, say Google or Twitter. On your dev machine, you don't have valid credentials or even cannot access a service due to network configuration.
How to write tests for such code? Moreover, how to ensure you application handles an incorrect HTTP response?
HTTP/1.1 403 OK
{"error": "wrong credentials"}
Or even non-JSON data (standard Nginx page):
HTTP/1.1 504 Gateway Timeout
<html><body><p>Gateway Timeout</p></body></html>
Will your application deal with such a response properly without crushing with 500 error? If you think it will, how could you guarantee that?
Now imagine you have modern micro-service architecture where each action requires collecting data across 3 internal web-services making API calls and to compose a final result.
Mockery helps to imitate such unusual behaviour and write unit tests that cover all the cases were mentioned.
Example
Mockery provides a with-mock macros. It substitutes a function with a
temporary one while the code block is being executed. Inside the code block, you
may call that function without performing unwanted I/O.
Under the hood, it relies on the standard Clojure clojure.core/with-redefs-fn function. The changes are visible across all the threads while macros works.
Quick example:
(with-mock mock {:target ::test-fn ;; your function to mock :return 42} ;; the result (test-fn 1 2 3)) ;; will return 42
The first parameter, mock in our example, is a variable name to bind a mock
object to. The second one is a map of options. The :target is the only
required key that should be a full-qualified keyword or symbol. For example,
:clojure.string/join or 'clojure.string/join. If you'd like to point a
function located at the same namespace, use double colon syntax as well:
::my-func (expands into something like :your.current.ns/my-func).
Features
Mockery works with the built-tin unit test framework as well:
(defn mock-google [f] (with-mock _ {:target :some.project/get-geo-point ;; makes API call to Google :return {:lat 14.2345235 :lng 52.523513}} ;; what your function should return instead (f))) (use-fixtures :each mock-google) (deftest ...)
During the test, every (get-geo-point ...) call will return just the data you
specified without network communication.
:return value could be a function that will be called to produce further
result.
(with-mock _ {:target :myapp.google/get-geo-point :return (fn [& _] 100500)} (myapp.google/get-geo-point)) ;; returns 100500
The library imitates throwing bith Java and Slingshot exceptions:
(with-mock mock {:target ::test-fn :throw (Exception. "boom")} (try (test-fn 1) (catch Exception e (println e)))) ;; it was thrown (with-mock mock {:target ::test-fn :throw {:type :domain/error ;; slingshot map :data "boom"}} (try+ (test-fn 1) (catch [:type :domain/error] data (println data)))) ;; process data map here
Add side effects (prints, logs) passing a :side-effect key with a function
without arguments:
(with-mock mock {:target ::my-func :return 42 :side-effect #(println "Hello!")} (my-func 1)) ;; in addition to the result, "Hello!" will appear.
Mockery helps to ensure the function was called the exact number of times with
proper arguments. The mock atom holds a state that changes while you call the
mocked function. It accumulates its arguments and the number of calls:
(with-mock mock {:target ::test-fn :return 42} (test-fn 1) (test-fn 1 2) (test-fn 1 2 3) @mock) ;; returns (some fields are skipped) {:called? true :call-count 3 :call-args '(1 2 3) ;; the last args :call-args-list '[(1) (1 2) (1 2 3)] ;; args history :return 42 ;; the last result :return-list [42 42 42] ;; the entire result history }
You may mock multiple functions at once using with-mocks macro:
(with-mocks [foo {:target ::test-fn} bar {:target ::test-fn-2}] (test-fn 1) (test-fn-2 1 2) (is (= @foo {:called? true :call-count 1 :call-args '(1) :call-args-list '[(1)] :target ::test-fn})) (is (= @bar {:called? true :call-count 1 :call-args '(1 2) :call-args-list '[(1 2)] :target ::test-fn-2})))
Other
For further reading, check out Mockery documentation and unit tests. Feel free to submit your issues/suggestions.
Mockery was written by Ivan Grishaev, 2018.