Paul C
Paul C

Reputation: 8507

How can you mock macros in clojure for tests?

I'd like to mock out a macro in a namespace.

For instance, clojure.tools.logging/error.

I tried with-redefs with no luck

(def logged false)

(defmacro testerror
  {:arglists '([message & more] [throwable message & more])}
  [& args]
  `(def logged true))

(deftest foo
 ...
 (with-redefs
      [log/error testerror]
   ...

That gave this error: CompilerException java.lang.RuntimeException: Can't take value of a macro

Upvotes: 6

Views: 1992

Answers (2)

Piotrek Bzdyl
Piotrek Bzdyl

Reputation: 13185

Amalloy provided you the answer for your direct question on how to mock a macro - you cannot.

However, you can solve your problem with other solutions (simpler than moving your whole application to component dependency injection). Let me suggest two alternative implementations (unfortunately, not very straightforward but still simpler than using component).

Mock the function called by logging macro

You cannot mock a macro but you can mock a function that will be used when the logging macro get expanded.

(require '[clojure.tools.logging :as log])
(require '[clojure.pprint :refer [pprint]])

(pprint (macroexpand `(log/error (Exception. "Boom") "There was a failure")))

Gives:

(let*
 [logger__739__auto__
  (clojure.tools.logging.impl/get-logger
   clojure.tools.logging/*logger-factory*
   #object[clojure.lang.Namespace 0x2c50fafc "boot.user"])]
 (if
  (clojure.tools.logging.impl/enabled? logger__739__auto__ :error)
  (clojure.core/let
   [x__740__auto__ (java.lang.Exception. "Boom")]
   (if
    (clojure.core/instance? java.lang.Throwable x__740__auto__)
    (clojure.tools.logging/log*
     logger__739__auto__
     :error
     x__740__auto__
     (clojure.core/print-str "There was a failure"))
    (clojure.tools.logging/log*
     logger__739__auto__
     :error
     nil
     (clojure.core/print-str x__740__auto__ "There was a failure"))))))

As you can see, the function that does actual logging (if a given level is enabled) is done with clojure.tools.logging/log* function.

We can mock it and write our test:

(require '[clojure.test :refer :all])

(def log-messages (atom []))

(defn log*-mock [logger level throwable message]
  (swap! log-messages conj {:logger logger :level level :throwable throwable :message message}))

(with-redefs [clojure.tools.logging/log* log*-mock]
  (let [ex (Exception. "Boom")]
    (log/error ex "There was a failure")
    (let [logged (first @log-messages)]
      (is (= :error (:level logged)))
      (is (= "There was a failure!" (:message logged)))
      (is (= ex (:throwable logged))))))

Use your logging library API to collect and inspect log messages

Your logging library API might provide features that would allow you to plug into in your test to collect and assert logging events. For example with java.util.logging you can write your own implementation of Handler that would collect all logged log records and add it to a specific (or root) logger.

Upvotes: 5

amalloy
amalloy

Reputation: 92147

You cannot do this. The point of macros is that they are expanded when the code is compiled, and after that they are gone. The original code that included a call to the macro is unrecoverable. You cannot retroactively redefine a macro at runtime: you're too late already.

An alternative approach, if you want to have swappable logging implementations, would be to use something like Component for dependency injection, and use a different logging component depending on whether you are running tests or running your real program. Arguably that's a bit heavy-handed, and maybe there is a simpler approach, but I don't know it.

Upvotes: 5

Related Questions