Joseph Wilk

Joseph Wilk

Things with code, creativity and computation.

Isolating External Dependencies in Clojure

Isolating external dependencies helps make testing easier. We can focus on a specific unit of code and we can avoid slow tests calling real services or databases.

Clojure provides many different ways of achieving isolation. Lets explore what’s possible:

Redefining functions

We can redefine vars and hence functions in a limited scope with with-redefs

The documentation suggests its usefulness in testing:

Useful for mocking out functions during testing.

Lets look at an example where we want to isolate a function that logs to file:

1
2
3
(defn log-fn [] #(spit "report.xml" %))

(defn log [string] ((log-fn) string))

And the test removing the dependency on the filesytem:

1
2
(with-redefs [log-fn (fn [data] data)]
  (log "hello"))

Its important to note that with-redefs are visible in all threads and it does not play well with concurrency:

with-redefs can permanently change a var if applied concurrently:

1
2
3
4
5
6
(defn ten-sixty-six [] 1066)
(doall 
  (pmap #(with-redefs [ten-sixty-six (fn [] %)] (ten-sixty-six))
        (range 20 100)))

(ten-sixty-six) ; => 49 Ouch!

The parallel redefs conflict with each other when setting back the var to its original value.

Another option is alter-var-root which globally redefines a var and hence a function. alter-var-root can alter functions in other namespaces.

Writing our test to use alter-var-root:

1
2
3
4
(alter-var-root
 (var log-fn)
 (fn [real-fn] ; We are passed the function we are about to stub.
   (fn [data] (println data))))

Its important to note we have to reset the var if we want to restore the system to its previous state for other tests.

Redefining Dynamic Vars

Using dynamic vars we can rebind the value and hence we can use this as an injection point. Again if we can rebind vars we can rebind functions. Note though that we have to mark those functions as dynamic:

1
2
3
4
5
6
7
8
9
;The real http request
(defn ^:dynamic http-get-request [url] http/get url)
(defn get [url] (http-get-request [url]))

(defn fake-http-get [url] "{}")

(fact "make a http get request"
  (binding [http-get-request fake-http-get]
    (get "/some-resource")) => "{}"))

Unlike alter-var-root and with-redefs dynamic vars are bound at a thread-local level. So the stubbings would only be visible in that thread. Which makes this safe for tests being run concurrently!

Atoms & Refs (Global vars in disguise)

While insidious, evil, malicious and ugly we could use atom or refs to contain a dependency.

1
2
3
(def cache (atom (fn [method, & args] (apply (resolve (symbol (str "memcache/" method))) args))))

(defn get [key] (@cache get key))

And in our test:

1
(reset! cache (fn [method, & args] (apply (resolve (symbol (str "fake-cache/" method))) args)))

Yuck, lets never speak of that again.

Midje

The Midje testing framework provides stubbing methods through provided. In the core of Midje this uses our previously visited alter-var-root.

Lets see how our example would look using Midje:

The code:

1
2
3
(defn log-fn [] #(spit "report.xml" %))

(defn log [string] ((log-fn) string))

And our test that uses provided:

1
2
3
4
(fact "it should spit out strings"
  (log "hello") => "hello"
  (provided
    (log-fn) => (fn [data] data)))

Its important to note that the provided is scoped in effect. It is only active during the form before the Midje “=>” assertion arrow.

Conceptually think of it like this:

1
2
3
(provided
  (log-fn) => (fn [data] (println data))
  (captured-output (log "hello"))) => (contains "hello")

Flexibility

Midjes provided gives very fine grained control of when a stub is used:

1
2
3
4
5
6
(do
  (log "mad hatter")   ;will use the stub
  (log "white rabbit") ;will not use the stub
)
(provided
  (log "mad hatter") => (fn [data] (println data)))

And we can go further and define argument matcher functions giving huge flexibility in when a stub should be run.

Safety

Midje validates your stubs and checks your not doing anything too crazy which would fundamentally break everything.

Higher order functions

We can isolate dependencies by passing in functions which wrap that dependency. This abstracts the details of the dependency and provides a point where we can inject our own functions which bypasses the dependency.

For example:

1
2
3
(defn extract-urn [data]
  (let [urn-getter #(:urn data)]
 (do-it urn-getter)))

In our tests:

1
  (do-it (fn [] 10))

Simple and beautiful.

Substituting namespaces

We can switch the namespace that a block of functions are evaluated in. Hence we can swap in a completely new implementation (such as a fake) by changing the namespace.

An example faking out a key value store:

1
2
(defn get-it [arg & [namespace]]
  ((ns-resolve (or namespace *ns*) 'get) arg))

A fake implementation of this service:

1
2
3
4
5
6
7
8
9
(ns test.cache.fake)

(def cache (atom {}))

(defn get [arg]
  (@cache arg))

(defn put [arg value]
  (reset! @cache (assoc @cache arg value)))

And our test:

1
2
(fact "it should do something useful"
  (get-it "1234" 'test.cache.fake) => "1234")

Alternatively if we don’t want the mess of injecting new namespaces into our functions we could change namespace aliases to achieve the same effect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(ns example
  (:require [cache.memcache :as memcache]))

(when (= (System/getenv "ENV") "TEST")
  (ns-unalias 'example 'memcache)
  (require 'test.cache.fake)
  (alias 'memcache 'test.cache.fake))

(defn get [arg]
  (memcache/get arg))

; ...

(when (= (System/getenv "ENV") "TEST")
  ;Cleanup our rebinding of memcache alias
  (ns-unalias 'memcache 'example))

When running our tests we mutate the behaviour of the system by setting the ENV environment variable to TEST.

Runtime Polymorphism

Switch behaviour based on a the type of an argument. During testing we can inject a specific type which will behave differently from the real system.

Clojure gives us protocols and multimethods to achieve this:

Protocols

1
2
3
4
5
6
7
8
9
10
11
12
13
(:require [[fake-service :as fake]
           [service      :as service]])

(defprotocol Service
  (do-get [this arg]))

(deftype FakeService []
  Service
  (do-get [this arg] (fake/do-get arg)))

(deftype RealService []
  Service
  (do-get [this arg] (service/do-get arg)))

And in our test:

1
(do-get (FakeService.) "cheshire")

Multimethods

Similar we can use the type of arguments to indicate different behaviour.

1
2
3
4
5
6
7
8
9
10
(:require [[fake-service :as fake]
           [service      :as service]])

(defmulti do-get (fn [service param] [(:Service service) param]))

(defmethod do-get [:FakeService] [service param]
  (fake/do-get param))

(defmethod do-get [:RealService] [service param]
  (service/do-get param))

And in our test:

1
(do-get (FakeService.) "rabbit")

Defining functions at Runtime

Using environment variables its possible to switch what functions are defined at runtime. def always defines a method at the top level of a namespace.

Here is an example inspired from Midje’s source code:

1
2
3
4
5
6
7
8
9
10
(defn init! []
  (case (System/getenv "ENV")
    "TEST"
    (do
      (def get [key]       (fake/get key))
      (def set [key value] (fake/set key value))
    ;; else
    (do
      (def get [key]       (memcache/get key))
      (def set [key value] (memcache/set key value))))))

We would run our tests with ENV=test lein test.

How should I isolate dependencies in Clojure?

Having explored what we can do, what should we do?

There are a number of choices and a lot depends on you’re programming and testing style:

The Purest form of isolation

Passing a function, functions that wrap our dependencies means we do not have to mutate the code under test. This is the ideal form of isolating. This is where we want to be.

But sometimes either aesthetics or control might make us look elsewhere.

Functions with many parameters can become ugly and cumbersome to use.

Using external libraries where we cannot have design the way we want it (though we can try by wrapping the heck out of any library).

Finally integration tests are hard if not impossible to do with this form of dependency isolation.

The Aesthetic small touch form of isolation

var-alter-root is (very) scary, but the guard rails of Midje make it an easy way to isolate dependencies. It also supports flexibility in how we stub functions based on the arguments they are called with (or completely ignore the arguments). This flexibility is extremely powerful and is a big plus for Midjes provided.

The danger ever present with this form of isolation is ignoring your tests telling you about a problem in your design.

The Simple small touch form of isolation

While Midje provides lots of power and flexibly it does so at the cost of slightly awkward syntax and a lot of crazy macros (I say this having stared into the heart of Midje). For example parallel functions do not work with provided.

with-redefs, binding and var-alter-root provide flexibly to handle different testing scenarios. and no prior knowledge of an external tool is required.

If you don’t need the power of Midje or fear its complexity you can happily use nothing but Clojure’s standard library. Maybe you will even learn something about how Clojure works!

The Java small touch form of isolation.

Since Clojure supports java interop its always possible to fall back to using Java, OO and dependency injection. If you love Java, this is a good path.

The Crazy Large touch form of isolation

Namespace switching is a shortcut to having to stub out every single method. In one sweep we redefine all the functions of a namespace. This might be more useful for integration tests than unit tests.

That shortcut does come at a cost, we still have to maintain our fake ns every time something changes in the real namespace and our production code is left wrapped in ns-resolve or a ugly switch based on Environment settings. Ugly!

I don’t recommend using this form of isolation regularly but in edge cases it can be very convenient, though people will still think you are crazy.

Comments