【问题标题】:How to test a clojure macro that uses gensyms?如何测试使用 gensyms 的 clojure 宏?
【发布时间】:2013-05-20 15:43:16
【问题描述】:

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

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

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

【问题讨论】:

    标签: testing macros clojure gensym


    【解决方案1】:

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

    【讨论】:

    • 感谢您愿意说“尝试不同的方法”。你的建议肯定得到了很多人的认同。我同意测试最终结果是一个很好的策略。 (但这并不是说同时测试扩展是不明智的;我认为两者一起可以提供更全面的测试。)对于这个问题,我确实想测试宏观扩展。这是否明智当然值得商榷(见上文)。
    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-03-15
      • 1970-01-01
      • 2014-05-13
      • 2011-10-04
      • 2011-07-24
      • 1970-01-01
      相关资源
      最近更新 更多