8

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

这是我的心理解释。人们不测试宏扩展的原因之一是它目前有点痛苦。一般而言,人们往往会在看似困难的情况下合理地反对做某事,而与它的内在价值无关。是的——这正是我问这个问题的原因!如果这很容易,我认为人们会更频繁地这样做。如果这很容易,他们就不太可能通过给出“不值得做”的答案来合理化。

我也理解“您不应该编写复杂的宏”的论点。当然。但我们希望人们不要去想“如果我们鼓励一种不测试宏的文化,那么这将阻止人们编写复杂的宏”。这样的论点是愚蠢的。如果您有一个复杂的宏扩展,那么测试它是否按预期工作是一件明智的事情。即使是简单的事情,我个人也不会接受测试,因为我经常对错误可能来自简单的错误感到惊讶。

4

2 回答 2

8

不要测试它是如何工作的(它的扩展),测试是否有效。如果您测试特定的扩展,您将被链接到该实施策略;相反,只需测试(m1 2 inc)返回 3,以及任何其他必要的测试用例来安慰你的良心,然后你就会对你的宏正在工作感到高兴。

于 2013-05-25T00:27:11.893 回答
4

这可以通过元数据来完成。您的宏输出一个列表,其中可以附加元数据。只需将 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了,我会看到实际的目标和目标,我可以目视检查。

于 2016-01-08T13:15:30.920 回答