编写测试#
测试对于提高程序的质量和可维护性很重要。它们验证程序的行为,也作为规范,以避免随时间的推移出现退步。
MoonBit 提供测试支持,使编写测试更加简单和容易。
测试块#
MoonBit 提供测试代码块,用于编写内联测试用例。例如:
test "test_name" {
assert_eq(1 + 1, 2)
assert_eq(2 + 2, 4)
debug_inspect([1, 2, 3], content="[1, 2, 3]")
}
测试代码块本质上是一个类型为 () -> Unit raise Error 的函数:它返回 Unit,并可能抛出 Error。它在执行 moon test 期间被调用,并通过构建系统输出测试报告。assert_eq 函数来自标准库;如果断言失败,它会打印错误消息并终止测试。字符串 "test_name" 用于标识测试用例,是可选的。
由 moon test 启动的测试以模块根目录作为当前工作目录。测试中的相对路径应从模块根目录解析,而不是从包目录或测试文件所在目录解析。
如果测试名称以 "panic" 开头,表示测试的预期行为是触发 panic,只有在触发 panic 时测试才会通过。例如:
test "panic_test" {
let _ : Int32 = Option::None.unwrap()
}
快照测试#
在指定预期值时,编写测试可能会很繁琐。因此,MoonBit 提供了三种快照测试。所有这些都可以使用 moon test --update 自动插入或更新。
快照 Show#
我们可以使用 inspect(x, content="x") 来检查实现了 Show 特征的任何内容。正如我们之前提到的,Show 是一个可以派生的内建特征,提供了 to_string来打印数据结构的内容。标记参数 content 可以省略,因为 moon test --update 会自动插入:
struct X {
x : Int32
} derive(Debug)
pub extend X with Debug::{to_repr}
test "show snapshot test" {
debug_inspect({ x: 10 }, content="{ x: 10 }")
}
快照 JSON#
派生的 Show 特征的问题是它不能对输出进行格式化,导致输出非常长。
解决方案是使用 @json.inspect(x, content=x)。其好处是生成的内容是一个 JSON 结构,在格式化后更易读。
enum Rec {
End
Really_long_name_that_is_difficult_to_read(Rec)
} derive(Debug, ToJson)
pub extend Rec with Debug::{to_repr}
pub extend Rec with ToJson::{to_json}
test "json snapshot test" {
let r = Really_long_name_that_is_difficult_to_read(
Really_long_name_that_is_difficult_to_read(
Really_long_name_that_is_difficult_to_read(End),
),
)
debug_inspect(
r,
content="Really_long_name_that_is_difficult_to_read(Really_long_name_that_is_difficult_to_read(Really_long_name_that_is_difficult_to_read(End)))",
)
json_inspect(r, content=[
"Really_long_name_that_is_difficult_to_read",
[
"Really_long_name_that_is_difficult_to_read",
["Really_long_name_that_is_difficult_to_read", "End"],
],
])
}
也可以实现自定义 ToJson 来保留必要的信息。
快照任何内容#
有时我们不仅要记录一个数据结构,还要记录整个过程的输出。
可以使用完整的快照测试使用 @test.T::write 和 @test.T::writeln 记录任何内容:
test "record anything" (t : @test.Test) {
t.write("Hello, world!")
t.writeln(" And hello, MoonBit!")
t.snapshot(filename="record_anything.txt")
}
这将在该包的 __snapshot__ 下创建一个具有给定文件名的文件:
Hello, world! And hello, MoonBit!
这也可以用于测试应用程序的输出,无论是创建图像、视频还是一些自定义数据。
请注意,@test.T::snapshot 应该在测试块的末尾使用,因为它总是引发异常。
基于性质的测试#
基于示例的测试检查特定输入,而基于性质的测试描述大量生成的输入都应满足的关系。在普通的 test 块中使用标准库的 quickcheck 包即可,无需添加额外的模块依赖。对于 _test.mbt 文件中的示例,在 moon.pkg 中添加以下导入:
import {
"moonbitlang/core/quickcheck",
} for "test"
检查性质#
加零应当保持整数不变:
test "adding zero preserves an integer" {
@quickcheck.check((x : Int32) => x + 0 == x, count=100, seed=2026)
}
使用 moon test 运行这个测试。check 会生成输入,并逐一调用性质函数。返回 true 表示该用例通过;返回 false 或抛出错误会使 check 报告失败。这里,count 指定检查 100 个用例,seed 使运行可以复现。生成的 Int32 值并不限于较小的数,也可能接近该类型的极限。
输入类型必须实现 Arbitrary 来生成值、Shrink 来简化失败的输入,以及 Debug 来显示这些输入。常见类型(包括 Int32)已提供这些实现。性质函数应保持确定性,并避免修改输入。
理解反例#
那么加一呢?x + 1 > x 对数学中的整数成立,但对 Int32 也成立吗?0、1、100 等较小的示例都会通过。下面使用 report 寻找反例。它返回结果而不是抛出测试失败,因此可以用快照记录失败报告,同时让演示本身保持为通过的测试:
test "adding one does not always increase an integer" {
let report = @quickcheck.report(
(x : Int32) => x + 1 > x,
count=100,
seed=2026,
)
debug_inspect(
report,
content=(
#|Falsified(
#| counterexample=2147483647,
#| tests=99,
#| size=98,
#| shrinks=0,
#| shrink_attempts=31,
#|)
),
)
}
Falsified 表示性质函数返回了 false。反例是 2147483647,即最大的 Int32。加一会溢出并回绕到 -2147483648,所以结果变小了,而非变大。这是对固定宽度整数运算的错误假设,不是加法实现的缺陷。
QuickCheck 还会尝试仍然导致失败的更简单输入,这个过程称为缩减(shrinking)。这里 shrinks=0:没有更小的输入能保留这个失败,因为只有最大的 Int32 不满足该性质。可选参数 max_shrinks 限制检查的候选输入数量;一般而言,缩减并不保证找到最小的反例。如果性质不成立时应当让测试套件失败,请使用 check 而不是 report。
保留回归测试#
要复现一次运行,需要保持种子、设置、性质函数以及工具链和库版本不变。不同版本生成的用例和缩减结果可能不同。一旦反例揭示了缺陷或错误的预期,就应将该具体输入保留在基于示例的测试中,并写明正确的预期行为:
test "adding one at the Int32 boundary" {
let x : Int32 = 2147483647
assert_eq(x + 1, -2147483648)
assert_eq(x.to_int64() + 1, 2147483648L)
}
第二个断言在加一之前转换为 Int64,其范围足以容纳这次计算的结果。如果在 Int32 加法完成后再转换结果就太晚了:溢出已经发生。
两种测试都应保留:回归测试始终检查已发现的用例,而性质测试可以通过其他种子和设置探索更多输入。生成的用例全部通过,并不能证明性质对所有输入都成立。
关于自定义生成器及更多选择有效性质的方法,请参阅 MoonBit 中基于性质的测试。
黑盒测试和白盒测试#
在开发库时,验证用户是否可以正确使用它是很重要的。例如,可能会忘记使类型或函数公开。这就是为什么 MoonBit 提供了黑盒测试,允许开发人员了解其他人的感受。
能够访问包中所有成员的测试称为白盒测试,因为我们可以看到一切。这样的测试可以内联定义,也可以在文件中定义,文件名以
_wbtest.mbt结尾。只能访问包中公开成员的测试称为黑盒测试。这样的测试需要在文件中定义,文件名以
_test.mbt结尾。
The WhiteBox test files (_wbtest.mbt) imports the packages defined in the import and wbtest-import sections of the package configuration (moon.pkg, or legacy moon.pkg.json).
The BlackBox test files (_test.mbt) imports the current package and the packages defined in the import and test-import sections of the package configuration (moon.pkg, or legacy moon.pkg.json).