在软件开发领域中,测试是确保质量与可靠性的必要环节。俗话说得好“工欲善其事,必先利其器”,测试工具越简单、用户友好度越高,开发者编写测试的意愿度就越高。
为了满足大家的测试需求,MoonBit 标准库最近引入了 inspect 函数,我们也称之为 expect 测试,它可以帮助程序员快速编写测试。
相比于 OCaml 和 Rust,MoonBit 提供了更加简洁高效的测试体验。我们的测试工具不仅操作简便,而且无需任何外部依赖,支持开箱即用,简化了测试流程。
这里简单介绍一下 MoonBit:
MoonBit 是国内首个工业级编程语言及其配套工具链(https://www.moonbitlang.cn/)
是由粤港澳大湾区数字经济研究院(福田)基础软件中心打造的AI原生的编程语言以及开发者平台。MoonBit自2022年10月推出以来,通过创新框架在程序语言界形成后发优势,已在编译速度、运行速度和程序体积上取得了显著的优势。MoonBit 平台的出现不仅仅作为一种编程语言,更提供一个完整的开发工具链,包括 IDE、编译器、构建系统、包管理器等。
接下来,让我们进一步了解 inspect 函数的使用。
忽略掉与位置相关的参数后,inspect 函数签名为:
pub fn inspect(obj : Show, ~content: String = "")
这里 obj 是任意一个实现 Show 接口的对象,~content 是一个可选参数,表示我们所期望的 obj 转化为字符串后的内容。听起来有点绕?让我们先来看看 inspect 的基本用法:
01 基本用法
首先,让我们使用 moon new hello 创建一个新项目
此时项目的目录结构如下:
.
├── README.md
├── lib
│ ├── hello.mbt
│ ├── hello_test.mbt
│ └── moon.pkg.json
├── main
│ ├── main.mbt
│ └── moon.pkg.json
├── moon.mod.json
打开 lib/hello_test.mbt,将内容替换为:
fn matrix(c: Char, n: Int) -> String {
let mut m = ""
for i = 0; i < n; i = i + 1 {
for j = 0; j < n; j = j + 1 {
m = m + c.to_string()
}
m += "\n"
}
m
}
这里,matrix 函数接受一个字符 c 和整数 n 作为参数,生成一个 n * n 大小的字符矩阵。
接下来,添加如下内容:
test {
inspect(matrix('🤣', 3))?
}
打开终端,执行 moon test 命令可以观察到类似如下输出:
Diff:
----
🤣🤣🤣
🤣🤣🤣
🤣🤣🤣
----
这里的输出展示了 matrix 函数的实际输出和 ~content 参数的差异,执行 moon test -u 或者 moon test --update 可以观察到 lib/hello_test.mbt文件中的测试块被自动更新成:
test {
inspect(matrix('🤣', 3), ~content=
#|🤣🤣🤣
#|🤣🤣🤣
#|🤣🤣🤣
#|
)?
}
让我们再把 n 改成 4,然后执行 moon test -u 可以观察到测试块被自动更新成:
test {
inspect(matrix('🤣', 4), ~content=
#|🤣🤣🤣🤣
#|🤣🤣🤣🤣
#|🤣🤣🤣🤣
#|🤣🤣🤣🤣
#|
)?
}
02 稍微复杂的例子
一般来说,写完一个函数后,我们需要为其编写一些单元测试。最简单的是断言测试,MoonBit 标准库中提供了 @assertion.assert_eq 函数,用于判断两个值是否相等。对于上述例子来说测试编写相对容易,因为我们可以轻易预测输出结果是什么。
接下来让我们看一个稍微复杂一点的例子:如何测试计算斐波那契数列第 n 项的函数。
首先在 lib 目录下新建一个 fib.mbt 的文件,然后粘贴如下内容:
fn fib(n : Int) -> Int {
match n {
0 => 0
1 => 1
_ => fib(n - 1) + fib(n - 2)
}
}
我们需要一些测试来验证我们的实现是否正确。如果用断言测试,那么编写测试的流程是什么呢?假设我们想测试输入为 10 时 fib的结果,我们会写下类似如下的代码:
test {
@assertion.assert_eq(fib(10), ???)?
}
当我们写下这个测试的时候,会立即遇到一个问题,我们并不知道 assert_eq 中右侧的期望值应该是什么。我们可以自己在纸上计算,或者找一份斐波那契数列参考列表、或者执行我们自己实现的 fib 函数。总之,不论用什么方法,我们需要得到 fib(10) 的期望值是 55,才能完成一个测试用例的编写。
此时 lib/fib.mbt 文件内容应为:
fn fib(n : Int) -> Int {
match n {
0 => 0
1 => 1
_ => fib(n - 1) + fib(n - 2)
}
}
test {
@assertion.assert_eq(fib(10), 55)?
}
然后执行 moon test 可以观察到如下输出:
Total tests: 2, passed: 2, failed: 0.
大家可以感受到,这里的反馈周期是很长的。通常来说,用这种方法编写测试并不是很愉悦。
对于 fib 这个例子我们可以相对容易找到一个正确的值作为参考。然而,大多数情况下,我们想测试的函数并没有其他“真值表”作为参考。我们需要做的是给这个函数提供输入,然后观察其输出是否符合我们的期望。这种模式比较常见,但其他工具链很少提供支持。因此,我们的 MoonBit 工具链提供了对这一测试模式的一等支持。通过使用 inspect 函数,我们只需要提供输入,而不需要提供期望值。
接下来,让我们用 inspect 函数编写输入分别为 8,9,10 时的测试用例,此时 lib/fib.mbt 文件的内容如下:
fn fib(n : Int) -> Int {
match n {
0 => 0
1 => 1
_ => fib(n - 1) + fib(n - 2)
}
}
test {
@assertion.assert_eq(fib(10), 55)?
}
test {
inspect(fib(8))?
}
test {
inspect(fib(9))?
}
test {
inspect(fib(10))?
}
通过执行:
moon test
可以观察到实际输出与 inspect 函数中 ~content的差异:
$ moon test
test username/hello/lib/fib.mbt::0 failed
expect test failed at path/to/lib/fib.mbt:10:3-10:18
Diff:
----
21
----
test username/hello/lib/fib.mbt::1 failed
expect test failed at path/to/lib/fib.mbt:14:3-14:18
Diff:
----
34
----
test username/hello/lib/fib.mbt::2 failed
expect test failed at path/to/lib/fib.mbt:18:3-18:19
Diff:
----
55
----
接下来,我们的工作变成了检查这个输出是否正确,如果确信这些输出是正确的,通过执行 moon test -u,在 lib/fib.mbt 文件中对应的测试块会被自动更新成:
test {
inspect(fib(8), ~content="21")?
}
test {
inspect(fib(9), ~content="34")?
}
test {
inspect(fib(10), ~content="55")?
}
通过这种编写测试然后立即获取反馈的模式,能够极大提升编写测试的愉悦感。
接下来,让我们来看一个修改函数行为导致输出变化的例子。
例如,我们现在想让 fib 的第一项从 1 而不是从 0 开始,首先我们将 fib 函数中的 0 => 0 修改为 0 => 1:
fn fib(n : Int) -> Int {
match n {
然后执行 moon test 可以看到 expect test 自动为我们展示了前后差异:
$ moon test
test username/hello/lib/fib.mbt::0 failed: FAILED:path/to/lib/fib.mbt:10:3-10:36 `89 == 55`
test username/hello/lib/fib.mbt::1 failed
expect test failed at path/to/lib/fib.mbt:14:3-14:33
Diff:
----
2134
----
test username/hello/lib/fib.mbt::2 failed
expect test failed at path/to/lib/fib.mbt:18:3-18:33
Diff:
----
3455
----
test username/hello/lib/fib.mbt::3 failed
expect test failed at path/to/lib/fib.mbt:22:3-22:34
Diff:
----
5589
----
Total tests: 5, passed: 1, failed: 4.
这里的输出结果发生了位移,符合期望,我们大概率可以确定输出是对的。于是通过执行 moon test -u 自动更新测试结果。
等等!为什么自动更新后,还有一个测试用例失败了呢?
Total tests: 5, passed: 4, failed: 1.
这是因为我们忘记了修改断言测试,与 expect 测试不同,断言测试并不会自动更新。我们需要手动将断言测试对应的测试块修改为:
test {
@assertion.assert_eq(fib(10), 89)?
}
从这个例子也能看出来,expect 测试是可以与断言测试协同工作的。
重新执行 moon test,可以看到全部测试都通过了。
Total tests: 5, passed: 5, failed: 0.
想象一下,如果我们之前有数百个断言测试,修改起来将会非常麻烦。通过使用 expect 测试,可以让我们从枯燥的更新工作中解脱出来。
以上,我们通过两个例子展示了它们如何通过编写测试并获得即时反馈,显著提升测试效率的能力。
我们鼓励你尝试使用 MoonBit 的 expect 测试,体验其在实际应用中的便利和高效。
推荐阅读:
https://blog.janestreet.com/the-joy-of-expect-tests/
你还可以了解更多内容:
- 如何开始使用 MoonBit
- 详细的 MoonBit 文档
- 添加微信小助手moonbit_helper,加入 MoonBit 用户群。
- MoonBit 标准库现已开源,查看指南了解如何进行贡献。