Rust入门(七):编写测试
admin
2024-05-06 19:01:37
0

Rust 中的测试函数是用来验证代码是否是按照你期望的方式运行的一类函数:

函数测试

Rust 中的测试就是一个带有 test 属性注解的函数,当使用 cargo test 命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了 test 属性的函数,并报告每一个测试是通过还是失败。

#[test]
fn it_works() {assert_eq!(2 + 2, 4);
}

函数体通过使用 assert_eq! 宏来断言 2 加 2 等于 4。一个典型的测试的格式,运行后可以看到,显示了生成的测试函数的名称,它是 it_works,以及测试的运行结果,ok。接着可以看到全体测试运行结果的摘要:test result: ok. 意味着所有测试都通过了。1 passed; 0 failed 表示通过或失败的测试数量。我们并没有将任何测试标记为忽略,所以摘要中会显示 0 ignored。我们也没有过滤需要运行的测试,所以摘要中会显示0 filtered out0 measured 统计是针对性能测试的

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.57sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::it_works ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

当测试函数中出现 panic 时测试就失败了,每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败

#[test]
fn another() {panic!("Make this test fail");
}

test tests::another 这一行是 FAILED 而不是 ok 了,只要有一个函数是 FAILED ,则整个函数的测试结果是 FAILED

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.72sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 2 tests
test tests::another ... FAILED
test tests::exploration ... okfailures:---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::anothertest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass '--lib'

用于测试的API

assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用,需要向 assert! 宏提供一个求值为布尔值的参数。如果值是 trueassert! 什么也不做,同时测试会通过。如果值为 falseassert! 调用 panic! 宏,这会导致测试失败

 #[test]
fn test2() {let re;if(2+2 == 4){re = true;}else{re = false}assert!(re);
}

assert_eq!assert_ne!。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 为什么失败,而 assert! 只会打印出它从 == 表达式中得到了 false 值,而不是导致 false 的两个值。

pub fn add_two(a: i32) -> i32 {a + 3
}
#[cfg(test)]
mod tests {use super::*;#[test]fn it_adds_two() {assert_eq!(4, add_two(2));}
}

测试捕获到了 bug!it_adds_two 测试失败,显示信息 assertion failed: (left == right) 并表明 left 是 4 而 right 是 5。这个信息有助于我们开始调试:它说 assert_eq! 的 left 参数是 4,而 right 参数,也就是 add_two(2) 的结果,是 5。

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.61sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::it_adds_two ... FAILEDfailures:---- tests::it_adds_two stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`left: `4`,right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::it_adds_twotest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass '--lib'

属性 should_panic 会让函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。#[should_panic] 属性位于 #[test] 之后,对应的测试函数之前

pub struct Guess {value: i32,
}impl Guess {pub fn new(value: i32) -> Guess {if value < 1 || value > 100 {panic!("Guess value must be between 1 and 100, got {}.", value);}Guess { value }}
}
#[cfg(test)]
mod tests {use super::*;#[test]#[should_panic]fn greater_than_100() {Guess::new(200);}
}
$ cargo testCompiling guessing_game v0.1.0 (file:///projects/guessing_game)Finished test [unoptimized + debuginfo] target(s) in 0.58sRunning unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)running 1 test
test tests::greater_than_100 - should panic ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests guessing_gamerunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

为了使 should_panic 测试结果更精确,我们可以给 should_panic 属性增加一个可选的 expected 参数。测试工具会确保错误信息中包含其提供的文本,如下的这个测试会通过,因为 should_panic 属性中 expected 参数提供的值是 Guess::new 函数 panic 信息的子串

impl Guess {pub fn new(value: i32) -> Guess {if value < 1 {panic!("Guess value must be greater than or equal to 1, got {}.",value);} else if value > 100 {panic!("Guess value must be less than or equal to 100, got {}.",value);}Guess { value }}
}#[cfg(test)]
mod tests {use super::*;#[test]#[should_panic(expected = "Guess value must be less than or equal to 100")]fn greater_than_100() {Guess::new(200);}
}

自定义错误信息

你也可以向 assert!assert_eq!assert_ne! 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来,如下的函数:

#[test]
fn greeting_contains_name() {let result = greeting("Carol");assert!(result.contains("Carol"),"Greeting did not contain name, value was `{}`",result);
}}

如果这个函数的运行结果是失败的,将会打印出自定义失败信息参数

$ cargo testCompiling greeter v0.1.0 (file:///projects/greeter)Finished test [unoptimized + debuginfo] target(s) in 0.93sRunning unittests (target/debug/deps/greeter-170b942eb5bf5e3a)running 1 test
test tests::greeting_contains_name ... FAILEDfailures:---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::greeting_contains_nametest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass '--lib'

Result测试

我们也可以使用 Result 编写测试,使用 Result 重写,并在失败时返回 Err 而非 panic,测试通过时返回 Ok(()),在测试失败时返回带有 StringErr

#[cfg(test)]
mod tests {#[test]fn it_works() -> Result<(), String> {if 2 + 2 == 4 {Ok(())} else {Err(String::from("two plus two does not equal four"))}}
}

控制测试运行

当运行多个测试时, Rust 默认使用线程来并行运行。这意味着测试会更快地运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。

举个例子,每一个测试都运行一些代码,假设这些代码都在硬盘上创建一个 test-output.txt 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中修改了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干扰。一个解决方案是使每一个测试读写不同的文件;另一个解决方案是一次运行一个测试。

如果你不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递 --test-threads 参数和希望使用线程的数量给测试二进制文件。

$ cargo test -- --test-threads=1

默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容。比如在测试中调用了 println! 而测试通过了,我们将不会在终端看到 println! 的输出:只会看到说明测试通过的提示行。如果测试失败了,则会看到所有标准输出和其他错误信息。

如果你希望也能看到通过的测试中打印的值,也可以在结尾加上 --show-output 告诉 Rust 显示成功测试的输出。

$ cargo test -- --show-output

有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行与这些代码相关的测试。你可以向 cargo test 传递所希望运行的测试名称的参数来选择运行哪些测试。

pub fn add_two(a: i32) -> i32 {a + 2
}#[cfg(test)]
mod tests {use super::*;#[test]fn add_two_and_two() {assert_eq!(4, add_two(2));}#[test]fn add_three_and_two() {assert_eq!(5, add_two(3));}#[test]fn one_hundred() {assert_eq!(102, add_two(100));}
}
$ cargo test one_hundredCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.69sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::one_hundred ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

我们可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行。例如,因为头两个测试的名称包含 add,可以通过 cargo test add 来运行这两个测试:

$ cargo test addCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.61sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

虽然可以通过参数列举出所有希望运行的测试来做到,也可以使用 ignore 属性来标记耗时的测试并排除他们

#[test]
#[ignore]
fn expensive_test() {// 需要运行一个小时的代码
}

我们在 #[test] 之后增加了 #[ignore] 行。现在如果运行测试,就会发现 it_works 运行了,而 expensive_test 没有运行:

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.60sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 2 tests
test expensive_test ... ignored
test it_works ... oktest result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

如果我们只希望运行被忽略的测试,可以使用 cargo test -- --ignored,如果你希望不管是否忽略都要运行全部测试,可以运行 cargo test -- --include-ignored

$ cargo test -- --ignoredCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.61sRunning unittests (target/debug/deps/adder-92948b65e88960b4)running 1 test
test expensive_test ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

测试组织架构

Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与 集成测试integration tests)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。

  • 单元测试

单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的某个单元的代码功能是否符合预期。

单元测试与他们要测试的代码共同存放在位于 src 目录下相同的文件中。规范是在每个文件中创建包含测试函数的 tests 模块,并使用 cfg(test) 标注模块。测试模块的 #[cfg(test)] 注解告诉 Rust 只在执行 cargo test 时才编译和运行测试代码,而在运行 cargo build 时不这么做,cfg 属性代表 configuration ,它告诉 Rust 其之后的项只应该被包含进特定配置选项中。在这个例子中,配置选项是 test

#[cfg(test)]
mod tests {#[test]fn it_works() {assert_eq!(2 + 2, 4);}
}
  • 集成测试

在 Rust 中,集成测试对于你需要测试的库来说完全是外部的。同其他使用库的代码一样使用库文件,也就是说它们只能调用一部分库中的公有 API ,集成测试的目的是测试库的多个部分能否一起正常工作。

为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。

例子如下:

创建一个 tests 目录,新建一个文件 tests/integration_test.rs,并输入示例 11-13 中的代码。文件名: tests/integration_test.rs,与单元测试不同,我们需要在文件顶部添加 use adder。这是因为每一个 tests 目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。tests 文件夹在 Cargo 中是一个特殊的文件夹, Cargo 只会在运行 cargo test 时编译这个目录中的文件。

use adder;#[test]
fn it_adds_two() {assert_eq!(4, adder::add_two(2));
}

也可以使用 cargo test--test 后跟文件的名称来运行某个特定集成测试文件中的所有测试:

$ cargo test --test integration_testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.64sRunning tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)running 1 test
test it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 目录创建集成测试并使用 extern crate 导入 src/main.rs 中定义的函数。只有库 crate 才会向其他 crate 暴露了可供调用和使用的函数;二进制 crate 只意在单独运行。

相关内容

热门资讯

英语翻译 他,已不在是曾经那个... 英语翻译 他,已不在是曾经那个少年他,已不再是曾经那个少年 【译】He himself is n...
火焰之纹章,21章秘密商店怎么... 火焰之纹章,21章秘密商店怎么进啊(我知道地点,就是进不去)?谢谢啦。盗贼身上没有会员卡啊? 是烈火...
起点大神级女作家都有谁 起点大神级女作家都有谁顺便说下他们的代表作~毕淑敏肯定算得上玖拾陆善终佞妆
有关网络小说 有关网络小说修真类小说和玄幻类小说有什么主要区别…从辩证角度来说:玄幻的不一定是修真的,但修真的一定...
一个女杀手的电影 一个女杀手的电影-----致命黑蓝
婵娟典故? 婵娟典故?婵娟是屈原的侍女,是一个纯洁可爱、天真稚气的少女。浮生萦云,她深明大义,爱憎分明,她热爱屈...
读了《我的战友邱少云》你认为这... 读了《我的战友邱少云》你认为这是一场什么样的战斗?你是从哪看出来的?邱少云为了战斗的最后胜利,被活活...
镇海楼的历史文物 镇海楼的历史文物 城防大炮:包括明朝崇祯年间以至清朝中叶鸦片战争广州地区铸造的防卫大炮,现放置於镇海...
艺术与审美的关系是什么? 艺术与审美的关系是什么?“美”是一个神圣的词,生活中因为有了美而变得丰富多彩,然而“美”又是一个抽象...
被黑洞吸嗜的东西都到哪去了? 被黑洞吸嗜的东西都到哪去了?黑洞吸嗜是物体衰亡的过程,当它衰变到一定程度,就会释放出来,形成新的物体...
妄念成灾是什么意思 妄念成灾是什么意思因为自己的一些不好的念头而造成了不好的事
白酒“降度潮”再现,行业自救还... (图片系AI生成) 新一轮“降度潮”正在席卷白酒行业,让一度沉寂的低度白酒重回大众视野。 6月,多家...
菠菜鸡蛋饼的做法和配方,菠菜鸡... 菠菜饼家常做法 1、菠菜汁和面:菠菜焯水后加少量水打成泥,过滤出菠菜汁(约150ml)。温水(35℃...
比网红店更打动人的,是街坊们吃... 重庆嘿好吃小店探店 第④②⑦期 20年历史的餐馆好吃妹儿还是吃过不少了,然而这次却遇到了一家号称开了...
进入初入,中老年人常吃这5样,... 1、五花肉蒸海昌鱼! 2、蒜蓉金针菇虾煲! 3、三杯鸡! 4、玉竹百合猪心汤! ...
山姆会员怒了!“我花钱办卡进会... 7月15日,话题“山姆下架多款口碑商品上新好丽友”登上热搜第一。 近日,山姆会员商店选品问题引发关...
演员蒋欣吃减脂餐“彩椒碗”,边... 7月15日一早, “关晓彤回复蒋欣”的词条, 冲上文娱热搜第一。 ↓ 7月14日晚,演员蒋欣在个人...
生孩子能带镯子吗,进手术室能不... 生孩子能带镯子吗,进手术室能不能戴玉镯子 如今越来越多的女性都喜欢在手上戴一些装饰品,尤其是经济...
大概的意思是什么? 大概的意思是什么?大概-释义释义1.大致的内容或情况:他嘴上不说,心里却捉摸了个~。2.属性词。不十...
郑州6个免费玩水好去处推荐!遛... 夏日炎炎大家是不是都想找个地方去耍水小编今天给大家推荐6个郑州免费玩水地遛娃、玩水、避暑纳凉一次性满...