简介
测试可以把预期转化为可执行的证据。有针对性的测试会调用一个行为,将实际结果与预期结果进行比较,并通过自动检测回归问题,让未来的修改更加安全。
你将为一个已经准备好的运费报价库添加测试。生产代码中的函数经过刻意简化,这样你可以专注于以下内容:Rust 测试文件的位置、断言如何表达预期、哪些边界情况值得测试,以及如何让 Cargo 一次运行一个测试目标。
添加有针对性的单元测试
在此步骤中,你将在库代码旁边添加一个单元测试,并且只运行这个指定名称的测试。
项目位于 /home/labex/project/shipping-quote。先进入该目录:
cd /home/labex/project/shipping-quote
编辑前,先查看简短的库源代码。这里使用 sed -n '1,220p' 只是为了只读查看:-n 会禁止自动打印,而 1,220p 会明确打印第 1 行到第 220 行。这个范围足以显示完整文件,无需打开编辑器:
sed -n '1,220p' src/lib.rs
写成 #[...] 的语法称为属性(attribute),表示附加到其下方 Rust 项的元数据。#[cfg(test)] 属性会告诉 Rust:只有在测试构建期间,才编译后面的测试模块。模块是用于组织相关项的命名容器;下一个 Guided Lab 会把模块作为通用的代码组织工具进行讲解。
在 mod tests 内部,路径关键字 super 表示父模块,* 表示该父模块中所有可访问的名称。因此,use super::*; 会让库函数可以在测试模块中直接通过短名称使用。最后,带有 #[test] 标记的函数会成为 Cargo 可以发现并运行的测试。
使用 Nano 打开源文件:
nano src/lib.rs
将 // FIRST_UNIT_TEST 替换为以下测试:
#[test]
fn light_parcel_costs_five() {
assert_eq!(shipping_cost(3), 5);
}
assert_eq! 会比较其中的两个表达式。如果两者相等,测试会继续执行;如果不相等,测试会失败并打印两个值。这里的测试记录了一条规则:运输一个 3 千克的包裹需要支付 5 个单位的费用。
按 Ctrl+O 保存,按 Enter 确认,然后按 Ctrl+X 退出。将测试名称作为 cargo test 的参数,可以只运行名称中包含该文本的测试:
cargo test light_parcel_costs_five
Cargo 会编译测试构建,并报告指定的单元测试:
test tests::light_parcel_costs_five ... ok
test result: ok. 1 passed; 0 failed; ...
tests:: 前缀表示该测试位于库的本地测试模块中,ok 表示断言已通过。
覆盖边界情况
在此步骤中,你将在运费行为发生变化的边界位置添加测试。
像 3 这样的代表性数值,只能验证轻量包裹范围内的一个普通值,但程序错误往往隐藏在边界上。在准备好的 match 中,1..=5 是一个范围模式(range pattern),可以匹配从 1 到 5 的任意值,包括两个端点。它把循环中使用的闭区间表示法应用到了新的模式场景中。该函数对 0 有特殊处理,并且包裹重量恰好为 5 千克时仍采用 5 个单位的费率。测试这些数值,就能记录这条规则的两个边界。
再次打开库文件:
nano src/lib.rs
将 // EDGE_UNIT_TESTS 替换为以下两个简短测试:
#[test]
fn empty_parcel_is_free() {
assert_eq!(shipping_cost(0), 0);
}
#[test]
fn five_kilograms_is_still_light() {
assert_eq!(shipping_cost(5), 5);
}
每个测试都在名称中描述一条规则,并包含一个有针对性的断言。这样可以更容易定位失败原因。保存文件并退出 Nano。
过滤器 tests:: 会匹配本地单元测试模块中测试的完整名称。使用它运行全部三个单元测试,同时暂时排除单独的集成测试目标:
cargo test tests::
确认操作成功的结果是三个单元测试全部通过:
test tests::empty_parcel_is_free ... ok
test tests::five_kilograms_is_still_light ... ok
test tests::light_parcel_costs_five ... ok
test result: ok. 3 passed; 0 failed; ...
这些示例共同覆盖了一个普通值,以及轻量包裹规则的两个重要边界。
完成集成测试
在此步骤中,你将从单独的文件中通过库的公共接口测试该库。
src/lib.rs 中的单元测试靠近实现代码,可以使用父模块中的内部项。集成测试位于顶层 tests/ 目录中,会编译为单独的测试程序,并且像外部调用方一样,只使用库的公共接口。包名称 shipping-quote 在源代码中会转换为 Rust crate 名称 shipping_quote,其中使用下划线。下一个 Lab 会集中讲解 package、crate、module 和 public visibility 这些术语。
查看已经准备好的集成测试框架:
sed -n '1,160p' tests/order_workflow.rs
该文件只导入公共函数 order_total,使用一个 40 单位的订单和一个 3 千克的包裹调用它,然后留下一条断言供你完成。打开该文件:
nano tests/order_workflow.rs
将以 // INTEGRATION_ASSERTION 结尾的占位断言行替换为:
assert_eq!(total, 45);
预期总额由 40 单位的商品小计和 5 单位的轻量包裹运费组成。保存文件并退出 Nano。
--test order_workflow 选项会选择集成测试目标,该目标对应文件 tests/order_workflow.rs:
cargo test --test order_workflow
该测试目标会报告公共工作流测试:
test order_total_includes_shipping ... ok
test result: ok. 1 passed; 0 failed; ...
最后,不使用过滤器,运行整个包的测试套件:
cargo test
Cargo 会运行三个单元测试和一个集成测试。由于它们属于不同的测试二进制文件,因此分别显示结果摘要是正常现象;每个测试名称后面都应显示 ok,并且每个摘要中的失败数量都应为 0。
总结
你添加了本地单元测试,覆盖了规则边界,通过库的公共 API 完成了集成测试,并使用 Cargo 过滤器运行单个测试、单个模块、单个集成测试目标或完整测试套件。这些习惯可以在开发过程中快速提供验证依据,并在交付前提供更广泛的回归保护。


