A handy (?) macro for testing

Hey there,

I just wanted to share this macro that solves a problem for me when I’m writing tests. I often find that I don’t want to do with-redefs to replace side-effecting functions because it’s not thread-bound. Instead, I have defined my side-effecting functions as ^:dynamic, which in turn clutters my code (the functions are not really meant to be dynamic).

So I whipped together this macro that “wraps” the side-effecting function with a check of whether it has been dynamically rebound to another function, and if so calls that function instead.

(defmacro replace-fn
  {:clj-kondo/lint-as 'clojure.core/def}
  [dyn-var orig-var]
  `(do
     (def ^:dynamic ~dyn-var nil)
     (alter-var-root (var ~orig-var)
                     (fn [f#]
                       (fn [& args#]
                         (if ~dyn-var
                           (let [x# ~dyn-var]
                             (binding [~dyn-var nil] ;; EDIT: bind to `nil` instead of `f#`
                               (apply x# args#)))
                           (apply f# args#)))))))

It works like this

(defn replace-me
  [y]
  (* y 2))

;; ^:dynamic is strictly speaking not 
;; needed here but it prevents linting errors
(replace-fn ^:dynamic *replacement* replace-me)

(replace-me 2)
;; => 4

(binding [*replacement* (fn [y]
                            (* 3 y))]
    (replace-me 2))
;; => 6

;; Inside the replacement function, the original function name
;; refers to the original function, not its replacement
(binding [*replacement* (fn [y]
                            (replace-me (* 3 y)))]
    (replace-me 2))
;; => 12

Hope someone finds this useful!

3 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.