Using binding to mock out even “direct linked” functions in Clojure
The Clojure macro binding is frequently handy for mocking out functionality during testing, but this sometimes does not behave as desired in multi-threaded context. Fortunately, there's a solution...
For example, at work we have unit tests on functions that may attempt to talk to network services, which we'd rather they not do:
(defn send-request [request server] '(...do real RPC stuff here...)) (defn average-timestamp [time-servers] (/ (apply + (map #(send-request :get-timestamp %) time-servers)) (count time-servers)))
In order to test average-timestamp, we need a "mock" function that will stand in for send-request. This must be a function that takes a request and a server and returns a timestamp, just like the real send-request. For this example, it can take the timestamp itself as the "server" and simply return that timestamp. For bonus points we can make sure the request parameter is what we expect:
(defn mock-send-request [request server] (assert (= request :get-timestamp)) server) ; assume "server" is actually the timestamp (mock-send-request :get-timestamp 5) ;=> 5
Using binding we can temporarily replace send-request with mock-send-request:
(binding [send-request mock-send-request] (send-request :get-timestamp 5)) ;=> 5 (binding [send-request mock-send-request] (average-timestamp [5 15])) ;=> 10
So there's the background: a pretty normal way to mock out stuff in Clojure. What this doesn't address is when some clever co-worker (hi, Nathan!) realizes that average-timestamp would work better if it talked to multiple time-servers in parallel, and that this could be easily accomplished by replacing the use of map with pmap:
(defn average-timestamp [time-servers] (/ (apply + (pmap #(send-request :get-timestamp %) time-servers)) (count time-servers)))
This is an easy single-letter change that indeed works quite well with the real send-request. But when we try to use binding to mock it out, we run into problems:
(binding [send-request mock-send-request] (average-timestamp [5 15])) ; java.lang.ClassCastException: ; clojure.lang.PersistentList cannot be cast to java.lang.Number
It's not obvious from the error message, but what's happening is our original un-mocked send-request is getting called. This is because binding only has an effect on the current thread, but pmap causes send-request to be called in other threads.
The solution is simple enough in Clojure 1.0 -- you simply mock out pmap as well, and since the API is identical to map, it's quite easy to do. But as you can see, this does us no good at all in recent versions of Clojure:
(binding [send-request mock-send-request pmap map] (average-timestamp [5 15])) ; java.lang.ClassCastException: ; clojure.lang.PersistentList cannot be cast to java.lang.Number
The reason is that starting with Clojure 1.1, most clojure.core Vars are linked directly into code that uses them. This means that our definition of average-timestamp above links directly to the actual definition of clojure.core/pmap, not just the Var that points to it, thus attempts to rebind the Var with binding are futile.
But do not despair, there is a solution even for this. All you need to do is tell Clojure not to directly link pmap when it compiles average-timestamp. This is done by adjusting pmap's metadata before average-timestamp is compiled, like so:
; Set pmap to be dynamically linked (alter-meta! #'pmap assoc :dynamic true) ; Must now re-define the function that uses pmap (defn average-timestamp [time-servers] (/ (apply + (pmap #(send-request :get-timestamp %) time-servers)) (count time-servers))) ; Finally, our mocking out of pmap works (binding [send-request mock-send-request pmap map] (average-timestamp [5 15])) ;=> 10
The best place to put the alter-meta! depends on your particular use case. You might want to figure out how to do the alter-meta! only when in testing and not in production. Remember that the metadata on pmap is global -- you're changing the only metadata that clojure.core/pmap has, so the namespace you're in when you do it makes no difference at all.
In our case at work however, the performance impact of leaving pmap dynamic all the time was not enough to worry about. Dynamic linking is still pretty fast and is fine even for our production uses of pmap in this particular code base. So we simply put the alter-meta! at the top of the file that used pmap and got on with our unit testing.
- The Joy of Clojure
- Thinking the Clojure Way
by Michael Fogus and Chris Houser