将测试代码翻译为文档

谢益辉 2018-08-02

我有一个大胆的想法。这个想法萌生于我召唤壮士帮我写测试时。我的 R 包的测试代码都是基于我自己的轻量级 R 包 testit,我不用流行的 testthat 是因为我觉得它把语法搞得太复杂,活生生造出一套词汇。所谓测试,无非就是当(函数)输出与预期不符时,就给个提醒,这个提醒通常是报错。testit 的测试语法很简单,只由一个函数 assert() 和一个特殊语法构成,形如

library(testit)
assert('某函数 foo() 应该输出某结果', {
  # 任意 R 代码
  
  (foo() == '预期结果')  # 测试条件
})

上面说的特殊语法就是,测试条件要写在一对小括号中,这是为了让 assert() 知道花括号内部哪些 R 代码是应该被检测的。每个测试条件必须返回元素全部为 TRUE 的逻辑向量(长度大于等于 1)。花括号里可以写任意 R 代码以及多个测试条件,每个条件都得在小括号里。如果测试条件返回的不全是 TRUE,那么 assert() 就会报错。原理就是这么简单。

一般情况下,只有开发者自己知道测试的存在,用户不会关心一个包里有哪些测试,但其实测试在某种程度上也是宝贵的文档,把测试代码埋在地底下有些可惜。此话怎讲?

文档通常是文字描述。文字描述的问题就是可能会不精确,不同的人理解起来也可能会不一样。测试则是同时给出了源代码和它的预期输出,若读者能同时读这两部分,那么也许可以增强他们对一个函数的理解。代码和数学公式一样,都是精确的表达形式(所以有那句话:“废话少说,放码过来”)。

那么问题来了。测试代码的形式通常比较乱,普通青年应该不会爱读这样的源代码。要是能把代码重新组织一下,做成一篇文章或报告的形式,可读性应该可以大大增加。以 testit 的测试为例:assert() 的第一个参数(字符串)可以作为小节标题,花括号内的任意 R 代码可以作为普通代码段,测试条件则可以作为表格或某种双栏形式呈现。比如上面的测试可以翻译为下面的 Markdown 文档:

# 某函数 foo() 应该输出某结果

```r
# 任意 R 代码
```

------- -----------
foo()     预期结果
------- -----------

更进一步,如果再用一点黑魔法,把这样的输出塞进相应的 R 帮助文档中,那么一个函数的帮助文档中不仅可以看文字文档、只有源代码的例子,还可以看相关的测试,让用户知道一个函数在不同条件下预期应该返回什么值。那就厉害了我的哥。

上面的从测试到文档的翻译咋整呢?因为 R 继承了 Lisp 的“代码本身就是数据”的特征,你有了代码之后想怎么解析、运行都可以,所以翻译应该不成问题(虽然我也没有十成把握)。我在 assert()查探测试条件的小括号其实不过就是用了 match.call() 和表达式(expression)的简单魔法。

当然,这整件事情还可以完全反过来做,也就是从文档到测试。这个在去年的 R OpenSci 小会上已经有几位黑客尝试过了