我想测试一个使用gensyms的宏。例如,如果我想测试一下:
(defmacro m1
[x f]
`(let [x# ~x]
(~f x#)))
我可以使用宏扩展......
(macroexpand-1 '(m1 2 inc))
......得到......
(clojure.core/let [x__3289__auto__ 2] (inc x__3289__auto__))
一个人很容易验证是否正确。
但是,我如何以实用,干净的自动方式进行测试? gensym不稳定。
(是的,我知道特定的宏观例子并不引人注目,但问题仍然是公平的。)
我意识到Clojure表达式可以被视为数据(它是一种同性语言),所以我可以将结果分开:
(let [result (macroexpand-1 '(m1 2 inc))]
(nth result 0) ; clojure.core/let
(nth result 1) ; [x__3289__auto__ 2]
((nth result 1) 0) ; x__3289__auto__
((nth result 1) 0) ; 2
(nth result 2) ; (inc x__3289__auto__)
(nth (nth result 2) 0) ; inc
(nth (nth result 2) 1) ; x__3289__auto__
)
但这很笨重。还有更好的方法吗?也许有数据结构'验证'库可以派上用场?也许解构会让这更容易?逻辑编程?
更新/评论:
虽然我很欣赏有经验的人说“不要测试宏观扩张本身”的建议,但它没有直接回答我的问题。
通过测试宏扩展“单元测试”宏有什么不好?测试扩展是合理的 - 事实上,许多人在REPL中“手动”测试他们的宏 - 那么为什么不自动测试呢?我没有理由不这样做。我承认测试宏扩展比测试结果更脆弱,但做前者仍然有价值。您也可以测试功能 - 您可以同时执行这两项功能!这不是一个/或决定。
这是我的心理解释。人们不测试宏观扩张的原因之一是它目前有点痛苦。一般来说,人们经常会在看似困难的时候采取合理的做法,而不论其内在价值如何。是的 - 这就是我问这个问题的原因!如果这很容易,我认为人们会更频繁地这样做。如果这很容易,他们就不太可能通过给出“不值得做”的答案来合理化。
我也理解“你不应该写一个复杂的宏”的论点。当然。但是,我们希望人们不要认为“如果我们鼓励不测试宏的文化,那么这将阻止人们编写复杂的宏。”这样的说法很愚蠢。如果你有一个复杂的宏扩展,测试它是否正常工作是一个明智的事情。我本人并不是在测试甚至是简单的东西,因为我常常惊讶于错误可能来自简单的错误。
答案 0 :(得分:7)
不要测试 它是如何工作的(它的扩展),测试 它的工作原理。如果您测试特定的扩展,那么您将被链接到该实施策略;相反,只需测试(m1 2 inc)
返回3,以及为了安抚你的良心所需的其他测试用例,然后你会对你的宏工作感到高兴。
答案 1 :(得分:3)
这可以通过元数据来完成。您的宏输出一个列表,该列表可以附加元数据。只需将gensym-> var映射添加到该映射,然后使用它们进行测试。
所以你的宏看起来像这样:
(defmacro m1 [x f]
(let [xsym (gensym)]
(with-meta
`(let [~xsym ~x]
(~f ~xsym))
{:xsym xsym})))
宏的输出现在有一个映射与gensyms:
(meta (macroexpand-1 '(m1 a b)))
=> {:xsym G__24913}
要测试宏扩展,您可以执行以下操作:
(let [out (macroexpand-1 `(m1 a b))
xsym (:xsym (meta out))
target `(clojure.core/let [~xsym a] (b ~xsym))]
(= out target))
要解决为什么要这样做的问题:我通常编写宏的方法是首先生成目标代码(即我希望宏输出的内容),测试做正确的事情,然后生成那个宏。预先知道良好的代码允许我对宏进行TDD;特别是我可以调整宏,运行测试,如果它们失败clojure.test
将显示实际与目标,我可以直观地检查。