Clojure宏来检查函数是否被调用

时间:2016-04-11 12:37:52

标签: clojure macros jvm

我一个月前开始使用Clojure,为了我的集成测试,我开发了一个小宏来检查是否从其他函数调用了函数。宏目前看起来像这样:

(defmacro called?
  [f val body]
  (let [flag (gensym `flag)]
    `(with-redefs-fn
      {~f (fn [& rest#] (def ~flag nil) ~val)}
      #(do
        (try
          (~body)
          (catch Throwable e#))
        (if (resolve '~flag)
          (bound? (resolve '~flag))
          false)))))

用法:

user=> (called? #'clojure.core/println nil #(println "hey"))
true
user=> (called? #'clojure.core/println nil #(print "hey"))
heyfalse

有没有办法在不爆炸堆栈的情况下调用原始实现?我尝试在第一个let中执行类似的操作,但这最终会导致ClassCastException clojure.lang.Cons cannot be cast to java.util.concurrent.Future

    (let [flag (gensym `flag)
          orig-f# (deref f)] ... )

1 个答案:

答案 0 :(得分:0)

我不确定你的目标是自己编写这样的宏,还是可以使用现有的库。

我建议conjure不仅允许你"spy" calls to a set of functions,还要验证哪些参数传递给这些函数:

(defn some-fn [n]
  (inc n))

(deftest test-instumenting
  (instrumenting [some-fn]
                 (is (= 43 (some-fn 42)))
                 (verify-called-once-with-args some-fn 42)))

conjure还支持模拟和存根,以便将测试与外部资源隔离开来。

如果你想自己实现它,我会遵循魔法的实现,这会促进一些良好的实践。

你的宏应该只是一个使用函数做主要工作的瘦语法糖层。创建包装函数,该函数将记录是否已调用原始函数。通过使用包装功能并单独存储计数,您可以稍后检查它,这样您就可以使原始功能保持不变并返回实际结果。

(def call-counts (atom {}))

(defn instrumented-fn [original-fn]
  (let [wrapping-fn (fn this [& args]
                      (swap! call-counts
                             update-in
                             [original-fn]
                             inc)
                      (apply original-fn args))]
    (swap! call-counts assoc original-fn 0)
    wrapping-fn))

    (defn have-been-called?
      [original-fn]
      (pos? (@call-counts original-fn 0))

    (defn reset-counters! [] (reset! call-counts {}))

使用实用程序功能设置,您可以使用宏添加精简语法糖层:

(defmacro instrumenting [f & body]
  `(with-redefs [~f (instrumented-fn ~f)]
     ~@body))

(defmacro called? [f & body]
  `(let [origin-fn# ~f]
     (instrumenting ~f ~@body)
     (have-been-called? origin-fn#)))

用法:

(defn my-fn [n] (inc n))

(have-been-called? my-fn)
;; => false

(instrumenting my-fn (dec 1))
;; => 0

(have-been-called? my-fn)
;; => false

(instrumenting my-fn (my-fn 1))
;; => 2

(have-been-called? my-fn)
;; => true

(reset-counters!)
(have-been-called? my-fn)
;; => false

(called? my-fn (dec 1))
;; => false

(called? my-fn (my-fn 1))
;; => true