我一个月前开始使用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)] ... )
答案 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