如何测试使用gensyms的clojure宏?

Dav*_* J. 7 testing macros clojure gensym

我想测试一个使用gensyms的宏.例如,如果我想测试这个:

(defmacro m1
  [x f]
  `(let [x# ~x]
    (~f x#)))
Run Code Online (Sandbox Code Playgroud)

我可以用宏扩展......

(macroexpand-1 '(m1 2 inc))
Run Code Online (Sandbox Code Playgroud)

...要得到...

(clojure.core/let [x__3289__auto__ 2] (inc x__3289__auto__))
Run Code Online (Sandbox Code Playgroud)

这对于一个人来说很容易验证是正确的.

但是,我如何以实用,干净的自动化方式对其进行测试?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__
  )
Run Code Online (Sandbox Code Playgroud)

但这很笨拙.还有更好的方法吗?也许有数据结构'验证'库可以派上用场?也许解构会让这更容易?逻辑编程?

更新/评论:

虽然我很欣赏有经验的人的建议,他们说"不要测试宏观扩张本身",但它没有直接回答我的问题.

通过测试宏扩展"单元测试"宏有什么不好?测试扩展是合理的 - 事实上,许多人在REPL中"手动"测试他们的宏 - 那么为什么不自动测试呢?我没有理由不这样做.我承认测试宏扩展比测试结果更脆弱,但做前者仍然有价值.您也可以测试功能 - 您可以同时执行这两项功能!这不是一个或两个决定.

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

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

ama*_*loy 8

不要测试它是如何工作的(它的扩展),测试是否有效.如果您测试特定的扩展,那么您将被链接到该实施策略; 相反,只需测试(m1 2 inc)返回3,并且无论其他测试用例是否需要安慰您的良心,然后您可以对您的宏工作感到高兴.


Ste*_*ith 5

这可以通过元数据来完成。您的宏输出一个列表,其中可以附加元数据。只需将 gensym->var 映射添加到其中,然后使用它们进行测试。

所以你的宏看起来像这样:

(defmacro m1 [x f]
  (let [xsym (gensym)]
    (with-meta 
      `(let [~xsym ~x]
         (~f ~xsym))
      {:xsym xsym})))
Run Code Online (Sandbox Code Playgroud)

宏的输出现在有一个带有 gensyms 的映射:

(meta (macroexpand-1 '(m1 a b)))
=> {:xsym G__24913}
Run Code Online (Sandbox Code Playgroud)

要测试宏扩展,您可以执行以下操作:

(let [out (macroexpand-1 `(m1 a b))
      xsym (:xsym (meta out))
      target `(clojure.core/let [~xsym a] (b ~xsym))]

  (= out target))
Run Code Online (Sandbox Code Playgroud)

要解决为什么要这样做的问题:我通常编写宏的方式是首先生成目标代码(即我希望宏输出的内容),测试它是否做正确的事情,然后从中生成宏。预先拥有已知良好的代码可以让我针对宏进行 TDD;特别是,我可以调整宏,运行测试,如果失败,clojure.test将向我显示实际与目标的对比,我可以目视检查。