如何测试使用gensyms的clojure宏?

时间:2013-05-25 00:01:04

标签: testing macros clojure gensym

我想测试一个使用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中“手动”测试他们的宏 - 那么为什么不自动测试呢?我没有理由不这样做。我承认测试宏扩展比测试结果更脆弱,但做前者仍然有价值。您也可以测试功能 - 您可以同时执行这两项功能!这不是一个/或决定。

这是我的心理解释。人们不测试宏观扩张的原因之一是它目前有点痛苦。一般来说,人们经常会在看似困难的时候采取合理的做法,而不论其内在价值如何。是的 - 这就是我问这个问题的原因!如果这很容易,我认为人们会更频繁地这样做。如果这很容易,他们就不太可能通过给出“不值得做”的答案来合理化。

我也理解“你不应该写一个复杂的宏”的论点。当然。但是,我们希望人们不要认为“如果我们鼓励不测试宏的文化,那么这将阻止人们编写复杂的宏。”这样的说法很愚蠢。如果你有一个复杂的宏扩展,测试它是否正常工作是一个明智的事情。我本人并不是在测试甚至是简单的东西,因为我常常惊讶于错误可能来自简单的错误。

2 个答案:

答案 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将显示实际与目标,我可以直观地检查。