OpenResty单元测试实践

无测试,不编码。有持续运行的单元测试,是保持项目健康最基本的要求。在多人协作的内部项目中,这一点尤其重要。
基于 OpenResty 的项目开发自然不会例外。

测试框架

我们考察过 OpenResty 自己的测试框架 test-nginx,发现该框架偏向于对接口进行测试。把它用作单元测试,犹如用园艺剪裁纸。
以 lua 应用的角度看,busted倒是个合适的单元测试框架。只是 OpenResty 项目代码中难免会用到 OpenResty 的 API。
如果不能在 OpenResty 上下文中运行测试,那么这些 API 就无法调用。显然我们不可能把这些 API 都分离出去,或者 mock 掉。
这么做不切实际。

好在 OpenResty 提供了 resty 命令行工具,能够以一次性命令的形式在 OpenResty 上下文运行给定的 lua 代码。
结合 restybusted 两个工具,有一个办法可以在 OpenResty 上下文中运行 busted 的测试代码。

首先,注意要安装 lua5.1 对应的 luarocks。luarocks 默认的 lua 版本是 5.2, 所以安装的时候需要配置一下。
如果能够像这样,直接指定使用安装 OpenResty 时附带的 luajit,那就更好了:

./configure --with-lua="/usr/local/openresty/luajit" --lua-suffix="jit" --with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1/

然后 sudo luarocks install busted, 安装 busted。

查看命令行工具 busted 的内容,会发现它其实是个启动脚本:

#!/usr/bin/env lua
-- Busted command-line runner
require 'busted.runner'({ standalone = false })

OK,现在就让 resty 去执行这个脚本吧!

假设项目结构如下:

..
├── src
│   └── code.lua
└── test
    └── test_spec.lua
    └── busted_runner.lua

其中 busted_runner.lua 的内容是:

require 'busted.runner'({ standalone = false })

我们可以这样运行:

# 当前工作目录是 test
resty -I ../src busted_runner.lua --verbose test_spec.lua

解释下,-I 参数后面跟着的是 lua 模块的路径,后面跟着的是要执行的代码文件和用户参数。所以上面的命令等价于 busted --verbose test_spec.lua

使用 resty 来运行 lua 代码有一个局限。它会在 ngx.timer 的上下文运行代码,导致许多跟请求上下文相关的 OpenResty API
依然是无法调用的。举个例子,如果待测试代码里面调用了 ngx.location.capture,执行测试会得到这样的结果:

Error → test_spec.lua @ 7
topic feature
test_spec.lua:15: API disabled in the current context

不过这已足矣,毕竟我们是在做单元测试而非接口测试。像是请求上下文的东西,就应该在测试时 mock 掉。至于怎么 mock,这属于
busted 的范畴,请参考 busted 的文档。

测试覆盖

有了测试,下面的需求就是统计测试的覆盖。

假若有具体的测试覆盖程度,程序员们可以针对性地编写测试,对哪些地方缺乏测试也心知肚明。
另外,还可以求出项目的测试覆盖率。
测试覆盖率不仅仅能够衡量项目的健康程度。覆盖率的增增减减,会激励程序员们尽可能地多写测试。
谁会愿意看着自己新增的代码一片红色(无测试覆盖)?自然而然的,如果每次提交的时候,可以及时获得测试覆盖的反馈,便能保持一个较高的覆盖率。

在 lua 中有一个库 luacov ,可以实现这样的功能。
在测试运行时加入 luacov ,它会在每一行加入钩子函数,触发对测试覆盖的统计。luacov 会把统计到的覆盖率报告在
luacov.stats.out 文件中。
busted已经内置了 luacov 支持,所以我们要操心的事情就少很多。

需要往前面的 busted 命令添加 --coverage 选项:

resty -I ../src -e 'require "busted.runner"({ standalone = false })' -- --coverage --verbose test_spec.lua

为了让 luacov 能够写出覆盖报告,还需要在 .luacov 下配置:

tick = true

如果不这么做,我们就没办法获取测试覆盖结果了。因为 luacov 默认只在程序退出时才写入覆盖报告,而我们的 openresty
是作为后台程序运行的。设置了 tick = true 后,luacov 会定期更新覆盖报告。除了 tick,你还可以在 .luacov
中设置许多 luacov 相关的配置:
http://keplerproject.github.i...

现在已经有了份 luacov.stats.out 了,它长这样:

127:/usr/local/share/lua/5.1/busted/init.lua
0 0 0 0 4 8 4 0 4 4 0 0 10 20 10 0 10 10 0 0 36 36 0 72 0 0 0 0 36 0 36 36 36 0 36 0 36 36 72 72 36 0 0 0 0 0 0 72 0 0 36 36 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 92
49:/usr/local/share/lua/5.1/busted/languages/en.lua
1 0 1 0 0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 0 1 0 1 0 0 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 1
23:/usr/local/share/lua/5.1/busted/modules/files/lua.lua
1 0 1 0 0 50 100 50 1 0 0 8 1 0 0 4 4 0 0 4 1 0 1
107:/usr/local/share/lua/5.1/busted/modules/files/moonscript.lua
1 0 2 1 1 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 1 0 1
...

显然,这一份不是人类可读的版本。

接着运行 luacov YOUR_SRC_DIR 可以生成出 luacov.report.out
这一份就是最终的测试覆盖率报告:

...
****0 for c, v in pairs(colorvalues) do
****0     colors[c] = makecolor(v)
      end

****0 return colors

==============================================================================
Summary
==============================================================================

File                                                              Hits Missed Coverage
...

当然这一份也不怎么“对人类友好”。如果嫌 luacov 默认生成的报告太粗糙,可以使用第三方的 reporter:
https://github.com/keplerproj...

或者,自己动手,丰衣足食:造一个 reporter 轮子。
这方面没有什么文档,只能去读luacov 默认的 reporter 实现
另外一个思路是,写个脚本去解析生成的 luacov.report.out 测试报告,去生成更加可视化的版本(或者跟现有的 CI 平台对接)。

相关推荐