A stubbing, spying, and mocking library for Clojure protocols that uses no macros, defines no vars, and is compatible with any test framework.
If you've ever unit-tested a stateful Clojure application, you may have found yourself needing to address side effects. For example, a function may query a database or invoke an external web service. One common way to manage these side effects is by declaring and implementing protocols that encapsulate the external call. This has the benefit of making them explicit and easily testable. Component is one framework that encourages this approach.
Although Clojure protocols are a great way to encapsulate operations with side effects, they suffer from a general lack of test tooling. Shrubbery provides a small set of basic building blocks for creating protocol implementations specifically designed for testing.
Shrubbery is test framework-agnostic, has no external dependencies, makes no attempt to perform runtime var replacement and uses no macros. It is simply meant to provide basic protocol implementation support for things like spies and stubs in the meagre hope that it makes your tests (and your day) slightly more pleasant.
stub
, which accepts a variable list of protocols and a optional hashmap of simple value implementations and returns an object that reifies all given protocols;spy
, which accepts an object with at least one protocol implementation and returns a new implementation that tracks the number of times each of its members were called (queryable withcalls
andreceived?
); andmock
, which wraps astub
in aspy
, allowing callers to supply basic function implementations and assert against those calls.
Spies and stubs can be used independently; any protocol implementation may be wrapped by a spy, and stubs need not themselves be spies.
If you aren't already using a library like Component to structure your Clojure application, or do not otherwise make much use of protocols, you may not find this library very interesting. Additionally, for more background on mocking as a strategy, you may find it helpful to read Martin Fowler's classic article on the subject, Mocks Aren't Stubs.
Shrubbery uses protocol reflection and eval
to perform dynamic reification without resorting to macros. Some
somewhat-obvious use cases suffer for having to drop down into syntax parsing––for example, in limiting the kinds of
simple values you can pass as stub implementations. If you know of a better way to reify protocol programmatically,
please get in touch.
Shrubbery is published to clojars.org.
Complete API docs are available. For a gentler introduction, see below.
Stubs are essentially sugar over reify
, allowing convenient dynamic protocol reification. By default,
a stub without implementations simply returns nil
for all method calls rather than throwing an
IllegalArgumentException
. Optionally, return values for some or all members of named protocols may be provided using a
map of method-name to value.
Once defined, stubs cannot currently be altered, though they may be introspected; see the Stub
protocol.
(defprotocol DbQueryClient
(select [client sql] "Returns a list of database records matching the given query."))
(defn find-user [client id]
(select client (str "select * from users where id = " id)))
(deftest test-find-user
(is (nil? (find-user (stub DbQueryClient) 42)))
(is (= "Guybrush" (-> (stub DbQueryClient {:select [{:name "Guybrush"}]}) (find-user 42) (first) (:name)))))
A test spy is an object that understands how many times its members have been called, and tracks the arguments those members were called with. Shrubbery spies take an object that implements at least one protocol, attempts to infer the protocols it implements, and returns a new implementation that proxies all calls to the starting implementation, tracking their usages along the way.
Note that Shrubbery provides no attempt to automatically verify call counts, as with classic mocking frameworks: currently, you as the test author are expected to perform your own assertions against the spy once the relevant methods have been called.
Spies are queried directly use calls
, which simply returns a hashmap of all calls, or indirectly using call-count
and received?
, both of which allow you to filter the count. Each function comes in two forms: given a
spy and a var referring to a function, it returns the appropriate calls; if additionally given a vector of expected
arguments, it will limit the query to calls matching those arguments.
Special support is provided for querying arguments against certain objects via the Matcher
protocol; in particular,
note that regular expressions are treated as if they are intended to be matched against strings rather than checked
for direct object equality. This is useful in cases where you might want to make a rough assertion against a string's
contents but don't care about an exact match.
As with stubs, once defined, spies cannot currently be altered, though they may be introspected; see the Spy
protocol.
(ns example
(:require [clojure.test :refer :all]
[shrubbery.core :refer :all]))
(defprotocol DbClient
(select [t sql]))
(def fake-db-client
(reify DbClient
(select [t sql] {:id 1})))
(defn find-user [client id]
(select client ...))
(deftest test-find-user
(let [subject (spy fake-db-client)]
(is (not (received? subject select)))
(is (= {:id 1} (find-user subject 42)))
(is (received? subject select))
(is (received? subject select [42]))))
Mocks combine the call count capabilities of a spy with the sugar of stubs. They act like a stub for the purposes of
initial definition but act like a spy for functions like call-count
and received?
. More precisely, they first
call stub
with the given arguments, then wrap that stub in a spy
.
(ns example
(:require [clojure.test :refer :all]
[shrubbery.core :refer :all]))
(defprotocol DbClient
(select [t sql]))
(defn find-user [client id]
(select client ...))
(deftest test-find-user
(let [subject (mock DbClient {:select {:id 1}})]
(is (not (received? subject select)))
(is (= {:id 1} (find-user subject 42)))
(is (received? subject select))
(is (received? subject select [42]))
))
Sometimes it's helpful, when checking the spies and mocks for the arguments they're called with, to perform partial
matching rather than full equality matching. Shrubbery defines a protocol, Matcher
, with one function, matches?
,
implements some special match cases. Regular expressions are one notable case where this is useful. For example:
(deftest test-db-client
(let [mock-db (mock DbClient)]
(select mock-db "select * from users")
(is (received? mock-db select ["select * from users"]))
(is (not (received? mock-db select ["select"])))
(is (received? mock-db select [#"select"]))
(is (not (received? mock-db select [#"select$"])))
(is (received? mock-db select [anything]))
))
Shrubbery's match?
function performs a standard =
check by default, but defines several special cases:
java.util.regex.Pattern
objects usere-seq
on the received objectclojure.lang.ArraySeq
objects performsmatch?
on all members of the received objectclojure.lang.Fn
objects are invoked directly with the received objectanything
is a special matcher that always returns true
Copyright © 2015 Brian Guthrie
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.