Sunday, June 30, 2013

Changing namespace aliases in Clojure

For unit testing my clojure code, I decided to try a little trick which I wasn't sure would work. I have a model namespace that defines the application state, which could be a rather large object that's computationally expensive to create and maintain. Rather than use with-redefs everywhere, I attempted to mock out the entire model namespace:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(ns myapp.routes.home-test
  (:require [clojure.test :refer :all]
            [myapp.routes.home :as routes]))

(ns myapp.routes.home-test.model-fake)

(def ^{:dynamic true :doc "Count of the number of ID generation calls"}
  *ids-generated* nil)

(defmacro with-test-context [& body]
  `(binding [*ids-generated* 0]
     ~@body))

(defn generate-id []
  (var-set *ids-generated* (+ *ids-generated* 1)))

(ns myapp.routes.home-test)

(alias 'fake 'myapp.routes.home-test.model-fake)

(use-fixtures :once
  (fn [f]
    (ns myapp.routes.home)
    (ns-unalias 'myapp.routes.home 'model)
    (alias 'model 'myapp.routes.home-test.model-fake)
    (try (f)
         (finally
           (ns myapp.routes.home)
           (ns-unalias 'myapp.routes.home 'model)
           (alias 'model 'myapp.model)))))

The idea was pretty straightforward:

  1. The source file name dictates the primary namespace, so the first form declares the namespace of our test cases.
  2. Next, we define a namespace that will serve as the model fake. We define a macro that defines a standardized test context, and then a set of functions that will be used to mock out the real namespace functions.
  3. Now, back in the main namespace, we create a fixture that switches to the namespace being tested, renames the model alias to point to the fake, runs the tests, and then switches the alias back to the correct namespace.

Sounds great in theory, but (fortunately) this doesn't work. I have verified that the namespace is indeed switched. However, clojure seems to compile in the namespace resolved symbol at the time of compilation. So while with-redefs works, switching namespace reference isn't sufficient.