Rust 测试
Rust 把测试功能直接做进了语言和 cargo 里,所以入门时不需要另外找一个测试库。这一页只讲刚好够用的内容,带你写出并运行第一个测试。
写一个最基本的测试
Section titled “写一个最基本的测试”测试就是一个标了 #[test] 的普通函数:
fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); }}有几点值得说明:
#[cfg(test)]告诉 Rust,这个模块只在运行测试时才编译,它不是正常程序的一部分。use super::*;会把父模块里的所有东西(这里是add函数)引入进来,这样测试才能调用它。#[test]把某个函数标记为测试,cargo test会运行它。
在项目文件夹里运行:
cargo test你会看到类似这样的输出:
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredassert! 和它的伙伴们
Section titled “assert! 和它的伙伴们”下面三个宏能满足你大部分的需求:
#[test]fn checks() { assert!(2 + 2 == 4); // 条件为 false 时,测试失败 assert_eq!(2 + 2, 4); // 两个值不相等时,测试失败 assert_ne!(2 + 2, 5); // 两个值相等时,测试失败}通常更推荐用 assert_eq! 和 assert_ne!,而不是简单的 assert!(a == b)。因为它们失败时会把两个值都打印出来,你一眼就能看出哪里不对,而不是只看到一句“assertion failed”(断言失败)。
失败的测试长什么样
Section titled “失败的测试长什么样”fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn this_will_fail() { assert_eq!(add(2, 2), 5); }}test tests::this_will_fail ... FAILED
assertion `left == right` failed left: 4 right: 5测试失败时,你想看到的正是这种信息:它告诉你代码实际算出了什么(left),而测试期望的是什么(right)。
测试函数会不会 panic
Section titled “测试函数会不会 panic”有时候,正确的行为就是 panic,比如一个应该拒绝非法输入的函数。#[should_panic] 就是用来测试这种情况的:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("不能除以零"); } a / b}
#[cfg(test)]mod tests { use super::*;
#[test] #[should_panic] fn dividing_by_zero_panics() { divide(10, 0); }}更好的失败信息
Section titled “更好的失败信息”测试失败时,你希望越快知道原因越好。两个小小的补充就很有帮助。
自定义信息。 assert!、assert_eq! 和 assert_ne! 都可以在值的后面再接一条信息,写法和 println! 一样。只有检查失败时才会打印它。
should_panic 的 expected。 单纯的 #[should_panic] 遇到任何 panic 都会通过,哪怕那是另一个 bug 引起的。加上 expected = "..." 之后,只有 panic 信息里包含这段文字,测试才会通过。
下面还是 divide 函数,加了一个 main 让你能运行它,另外有两个测试分别用到了这两个技巧:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("不能除以零"); } a / b}
fn main() { println!("10 / 2 = {}", divide(10, 2)); println!("7 / 2 = {}", divide(7, 2));}
#[cfg(test)]mod tests { use super::*;
#[test] fn rounds_down() { let result = divide(7, 2); assert_eq!(result, 3, "7 / 2 应该向下取整,但得到的是 {result}"); }
#[test] #[should_panic(expected = "除以零")] fn dividing_by_zero_panics() { divide(10, 0); }}输出:
10 / 2 = 57 / 2 = 3上面的输出是 cargo run 打印的,因为 main 不会运行测试。要运行测试,请用 cargo test。在 Rust Playground 里,打开 Run 旁边的菜单,选择 Test。你会看到:
running 2 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out测试是并行运行的,所以两行 test ... 的顺序可能会变。现在假设测试期望的是 4 而不是 3。失败信息里就会出现你写的那句话,紧挨着两个值:
assertion `left == right` failed: 7 / 2 应该向下取整,但得到的是 3 left: 3 right: 4如果 divide panic 时的信息不一样,expected 检查就会发现(这里假设写的是 expected = "负数"):
note: panic did not contain expected string panic message: "不能除以零" expected substring: "负数"这几行是 Rust 自己输出的,意思是:“panic 信息里没有包含预期的字符串。”下面分别列出了实际的 panic 信息和期望包含的子串。
为什么要写测试
Section titled “为什么要写测试”说实话:如果只是一个很短的脚本,你大概不会写测试。一旦代码要被复用或者以后会修改,测试就开始显出价值了。有了测试,你可以放心地重构,因为一旦改坏了什么,cargo test 会立刻告诉你,而不用等用户来告诉你。
测试也可以写在文档里:/// 文档注释里的代码示例同样会由 cargo test 运行,所以你的示例永远不会过时。Rust 文档一页会教你怎么做。
两道练习的答案里都有一个可以运行的 main,还有用 cargo test(或 Playground 里的 Test)运行的测试。
1. 测试一个评分函数
Section titled “1. 测试一个评分函数”写一个 fn grade(score: u32) -> char:90 分及以上返回 'A',80 到 89 分返回 'B',70 到 79 分返回 'C',更低的返回 'F'。然后写测试检查边界值,比如 90 和 89。提示:bug 最喜欢藏在边界上。专门测试正好 90 分,就能抓住把 >= 写成 > 这种经典错误。
查看答案
fn grade(score: u32) -> char { if score >= 90 { 'A' } else if score >= 80 { 'B' } else if score >= 70 { 'C' } else { 'F' }}
fn main() { for score in [95, 85, 72, 40] { println!("{score} -> {}", grade(score)); }}
#[cfg(test)]mod tests { use super::*;
#[test] fn edges_of_a() { assert_eq!(grade(90), 'A'); assert_eq!(grade(89), 'B'); }
#[test] fn edges_of_c() { assert_eq!(grade(70), 'C'); assert_eq!(grade(69), 'F'); }
#[test] fn zero_is_f() { assert_eq!(grade(0), 'F', "0 分应该是不及格"); }}输出:
95 -> A85 -> B72 -> C40 -> Fcargo test 应该报告 3 passed。试着把 score >= 90 改成 score > 90,再运行一次测试:edges_of_a 会失败,并显示 left: 'B' 和 right: 'A'。
2. 测试一个返回 Option 的函数
Section titled “2. 测试一个返回 Option 的函数”写一个 fn safe_divide(a: i32, b: i32) -> Option<i32>,当 b 为 0 时返回 None,而不是 panic。写一个测试检查正常的除法,再写一个测试检查除以零的情况。提示:assert_eq! 也能用于 Option,所以可以写 assert_eq!(safe_divide(1, 0), None)。
查看答案
fn safe_divide(a: i32, b: i32) -> Option<i32> { if b == 0 { None } else { Some(a / b) }}
fn main() { println!("{:?}", safe_divide(10, 2)); println!("{:?}", safe_divide(1, 0));}
#[cfg(test)]mod tests { use super::*;
#[test] fn divides_normally() { assert_eq!(safe_divide(10, 2), Some(5)); }
#[test] fn zero_gives_none() { assert_eq!(safe_divide(1, 0), None); }}输出:
Some(5)None和上面的 divide 函数对比一下。那里除以零会 panic,所以测试需要 #[should_panic]。这里它只是一个普通的返回值,用简单的 assert_eq! 就够了。返回 Option 或 Result 的函数通常更容易测试。
- 给函数标上
#[test],它就成了测试;用cargo test运行所有测试。 assert!、assert_eq!和assert_ne!能完成日常大部分检查。优先用assert_eq!/assert_ne!,失败信息更清楚。#[should_panic]用来测试函数在该 panic 的时候确实 panic 了。加上expected = "...",还能确认它是因为正确的原因 panic 的。- 可以给任何断言宏加一条自定义信息,在失败时说明出了什么问题。
- 对那些你预计以后会修改或复用的代码,测试最有价值。
小测验
1. 哪个属性能把函数标记为 cargo test 要运行的测试?
2. 为什么通常更推荐用 assert_eq! 和 assert_ne!,而不是简单的 assert!(a == b)?
3. #[should_panic] 有什么作用?
得分: 0 / 3
- 如何编写测试 — Rust 程序设计语言:
#[test]、断言宏、自定义信息、should_panic,以及返回Result的测试。 - 控制测试如何运行 — Rust 程序设计语言:按名字只运行某个测试、显示
println!的输出,以及#[ignore]。 - 测试的组织结构 — Rust 程序设计语言:单元测试和放在
tests/文件夹里的集成测试有什么区别。 - 单元测试 — 通过例子学 Rust:通过、失败和 panic 的测试的简短示例。
cargo test— The Cargo Book(英文):cargo test命令的所有选项。