(这是一种新方法,eval- 和 binding-free。如在
这个答案的 cmets,使用 eval 是有问题的,因为
它可以防止测试关闭它们看起来的词汇环境
定义在(所以(let [x 1] (deftest easy (is (= x 1))))没有
更长的作品)。我将原始方法留在了下半部分
答案,低于水平线。)
macrolet 方法
实施
使用 Clojure 1.3.0-beta2 测试;它可能应该与 1.2.x 一起使用
好吧。
(ns deftest-magic.core
(:use [clojure.tools.macro :only [macrolet]]))
(defmacro with-test-tags [tags & body]
(let [deftest-decl
(list 'deftest ['name '& 'body]
(list 'let ['n `(vary-meta ~'name update-in [:tags]
(fnil into #{}) ~tags)
'form `(list* '~'clojure.test/deftest ~'n ~'body)]
'form))
with-test-tags-decl
(list 'with-test-tags ['tags '& 'body]
`(list* '~'deftest-magic.core/with-test-tags
(into ~tags ~'tags) ~'body))]
`(macrolet [~deftest-decl
~with-test-tags-decl]
~@body)))
用法
...最好通过一组(通过)测试来证明:
(ns deftest-magic.test.core
(:use [deftest-magic.core :only [with-test-tags]])
(:use clojure.test))
;; defines a test with no tags attached:
(deftest plain-deftest
(is (= :foo :foo)))
(with-test-tags #{:foo}
;; this test will be tagged #{:foo}:
(deftest foo
(is true))
(with-test-tags #{:bar}
;; this test will be tagged #{:foo :bar}:
(deftest foo-bar
(is true))))
;; confirming the claims made in the comments above:
(deftest test-tags
(let [plaintest-tags (:tags (meta #'plain-deftest))]
(is (or (nil? plaintest-tags) (empty? plaintest-tags))))
(is (= #{:foo} (:tags (meta #'foo))))
(is (= #{:foo :bar} (:tags (meta #'foo-bar)))))
;; tests can be closures:
(let [x 1]
(deftest lexical-bindings-no-tags
(is (= x 1))))
;; this works inside with-test-args as well:
(with-test-tags #{:foo}
(let [x 1]
(deftest easy (is true))
(deftest lexical-bindings-with-tags
(is (= #{:foo} (:tags (meta #'easy))))
(is (= x 1)))))
设计说明:
我们想要制作基于 macrolet 的设计
问题文本工作。我们关心能够筑巢
with-test-tags 并保留定义测试的可能性
其主体封闭在它们被定义的词汇环境之上
在。
我们将macroletting deftest 扩展为
clojure.test/deftest 附有适当元数据的表单
测试的名称。这里重要的部分是with-test-tags
将适当的标签集直接注入到
在macrolet 表单中自定义本地deftest;一旦
编译器开始扩展deftest 形式,标记集
将被硬连线到代码中。
如果我们把它留在那里,测试定义在嵌套的
with-test-tags 只会被传递给
最里面的with-test-tags 表单。因此我们也有with-test-tags
macrolet 符号 with-test-tags 本身的行为很像
本地deftest:它扩展为对顶层的调用
with-test-tags 带有适当标签的宏注入到
标签集。
-
本意是内层with-test-tags形式在
(with-test-tags #{:foo}
(with-test-tags #{:bar}
...))
扩展至(deftest-magic.core/with-test-tags #{:foo :bar} ...)
(如果确实 deftest-magic.core 是命名空间 with-test-tags
定义在)。这种形式立即扩展为熟悉的
macrolet 形式,带有 deftest 和 with-test-tags 符号
本地绑定到具有正确标签集的宏
他们。
(原始答案更新了一些关于设计的注释,一些
改写和重新格式化等。代码不变。)
binding + eval 方法。
(有关版本,另请参阅 https://gist.github.com/1185513
另外使用macrolet 来避免自定义顶级
deftest.)
实施
以下内容经过测试可与 Clojure 1.3.0-beta2 一起使用;与
^:dynamic 部分已删除,它应该适用于 1.2:
(ns deftest-magic.core)
(def ^:dynamic *tags* #{})
(defmacro with-test-tags [tags & body]
`(binding [*tags* (into *tags* ~tags)]
~@body))
(defmacro deftest [name & body]
`(let [n# (vary-meta '~name update-in [:tags] (fnil into #{}) *tags*)
form# (list* 'clojure.test/deftest n# '~body)]
(eval form#)))
用法
(ns example.core
(:use [clojure.test :exclude [deftest]])
(:use [deftest-magic.core :only [with-test-tags deftest]]))
;; defines a test with an empty set of tags:
(deftest no-tags
(is true))
(with-test-tags #{:foo}
;; this test will be tagged #{:foo}:
(deftest foo
(is true))
(with-test-tags #{:bar}
;; this test will be tagged #{:foo :bar}:
(deftest foo-bar
(is true))))
设计说明
我认为在这种情况下,明智地使用 eval 会导致
有用的解决方案。基本设计(基于“binding-able Var”
想法)具有三个组成部分:
可动态绑定的 Var -- *tags* -- 在编译时绑定
是时候给deftest表单使用一组标签来装饰
正在定义的测试。我们默认不添加标签,所以它的初始
值为#{}。
一个with-test-tags 宏,它安装了一个合适的
*tags*.
-
一个自定义的 deftest 宏,它扩展为类似于 let 的形式
this(以下是展开,稍微简化一下
清晰度):
(let [n (vary-meta '<NAME> update-in [:tags] (fnil into #{}) *tags*)
form (list* 'clojure.test/deftest n '<BODY>)]
(eval form))
<NAME> 和 <BODY> 是自定义的参数
deftest,通过取消引用插入到适当的位置
语法引用的扩展模板的适当部分。
因此,自定义deftest 的扩展是let 形式,其中,
首先,新测试的名称是通过装饰给定的
带有:tags 元数据的符号;然后是clojure.test/deftest 表格
使用这个修饰名被构造;最后是后一种形式
已交给eval。
这里的重点是这里的(eval form)表达式是
每当它们包含的命名空间是 AOT 编译或
在运行它的 JVM 的生命周期中第一次需要
代码。这与(println "asdf") 完全相同
顶级(def asdf (println "asdf")),将打印asdf
每当命名空间被 AOT 编译或第一次需要
时间;事实上,顶级(println "asdf") 的作用类似。
这可以通过注意到在 Clojure 中的编译只是
评估所有顶级表格。在(binding [...] (deftest ...),
binding 是顶级表单,但仅在 deftest 时返回
确实如此,并且我们的自定义 deftest 扩展为一个表单,该表单在
eval 确实如此。 (另一方面,require 执行顶层的方式
已编译命名空间中的代码——因此,如果您的代码中有 (def t
(System/currentTimeMillis)),则 t 的值将
取决于您何时需要命名空间,而不是何时需要
已编译,可以通过使用 AOT 编译的代码进行试验来确定
-- 这正是 Clojure 的工作方式。如果你想要实际的,请使用 read-eval
嵌入在代码中的常量。)
实际上,自定义deftest 运行编译器(通过eval)
宏扩展的运行时编译时。有趣。
最后,当deftest 表单放入with-test-tags 表单时,
(eval form) 的 form 将已准备好绑定
由with-test-tags 安装到位。因此定义了测试
将使用适当的标签集进行装饰。
在 REPL
user=> (use 'deftest-magic.core '[clojure.test :exclude [deftest]])
nil
user=> (with-test-tags #{:foo}
(deftest foo (is true))
(with-test-tags #{:bar}
(deftest foo-bar (is true))))
#'user/foo-bar
user=> (meta #'foo)
{:ns #<Namespace user>,
:name foo,
:file "NO_SOURCE_PATH",
:line 2,
:test #<user$fn__90 user$fn__90@50903025>,
:tags #{:foo}} ; <= note the tags
user=> (meta #'foo-bar)
{:ns #<Namespace user>,
:name foo-bar,
:file "NO_SOURCE_PATH",
:line 2,
:test #<user$fn__94 user$fn__94@368b1a4f>,
:tags #{:foo :bar}} ; <= likewise
user=> (deftest quux (is true))
#'user/quux
user=> (meta #'quux)
{:ns #<Namespace user>,
:name quux,
:file "NO_SOURCE_PATH",
:line 5,
:test #<user$fn__106 user$fn__106@b7c96a9>,
:tags #{}} ; <= no tags works too
为了确保正在定义工作测试...
user=> (run-tests 'user)
Testing user
Ran 3 tests containing 3 assertions.
0 failures, 0 errors.
{:type :summary, :pass 3, :test 3, :error 0, :fail 0}