Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 280
marathon Avatar asked Feb 05 '23 06:02

marathon


1 Answers

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.

like image 100
Piotrek Bzdyl Avatar answered Feb 14 '23 10:02

Piotrek Bzdyl