Rust入门(七):编写测试

news/2024/11/22 19:10:17/

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<T, E> 编写测试,使用 Result<T, E> 重写,并在失败时返回 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 只意在单独运行。


http://www.ppmy.cn/news/9640.html

相关文章

git---常用命令集合

适用平台:gitee github gerrit gitlab 提交代码和查看相关信息 git log git status git add . 增加所有修改,需要添加指定文件可以选择添加文件即可 git commit -m "xxx" git push git reset --hard commitid 保持与服务器更新到commitid git pull git diff HEAD^ …

JavaScript篇.day10-面向对象,对象,构造函数,this关键字,原型

目录面向对象对象构造函数this关键字原型面向对象面向过程: 在开发过程中,关注过程的开发方式. 在开发时关注每一个细节,步骤和顺序.面向对象: 在开发过程中,只需要找一个对象来完成事情的开发思想对象: 在生活中,万物皆对象 封装: 将完成步骤封装在对象内部属性: 对象的特征核…

JavaWeb项目 -- 博客系统

JavaWeb项目 -- 博客系统前言&#xff1a;页面展示一、创建 Maven 项目二、设计数据库三、封装数据库的操作3.1 创建 DBUtil 类3.2 创建 Blog 类3.3 创建 User 类3.4 创建类 BlogDao3.5 创建类 UserDao四、导入准备好的前端代码五、实现博客列表界面5.1 约定好前后端交互接口5.…

简单又好用的财务分析工具有哪些?

什么样的财务分析工具才能算是简单又好用&#xff1f;是能够快速完成组合多变的财务指标运算分析&#xff1b;能够充分发挥企业经营健康晴雨表作用&#xff0c;反映企业财务健康状态&#xff1b;还是能够支持多维度动态分析、自助分析&#xff1b;或者是轻松合并账套&#xff0…

Java集合类ArrayList应用 | 如何在字符串s1中删除有在字符串s2出现的字符?

目录 一、题干 二、题解 1. 思路 ArrayList实现 2. 代码 ArrayList实现 StringBuilder实现-1 StringBuilder实现-2 三、总结 一、题干 面试的编程题&#xff1a; s1: "welcome to Zhejiang" s2: "come" 要求输出从字符串s1删除s2中存在的字符之后…

【Java寒假打卡】Java基础-BigDecimal

【Java寒假打卡】Java基础-BigDecimal构造方法四则运算BigDecimal的特殊方法基本数据类型包装类自动装箱与自动拆箱Integer的类型转换将数字字符串进行拆分成整数数组构造方法 package com.hfut.edu.test1;import java.math.BigDecimal;public class test3 {public static void…

Spring AOP 面向切面编程

1.AOP是什么我们之前学过 OOP 面向对象编程, 那么 AOP 是否就比 OOP 更牛逼呢? 是否也是面向过程到面向对象这种质的跨越呢? 其实不是, 它和 OOP 思想是一种互补的关系, 是对 OOP 思想的一种补充.AOP (Aspect Oriented Programming) : 面向切面编程, 它是一种思想, 它是对某一…

3台机器配置hadoop集群_Hadoop+Hbase 分布式集群架构

安装搭建Hadoop1、 配置说明本次集群搭建共三台机器&#xff0c;具体说明下&#xff1a;主机名IP说明nn01192.168.1.51DataNode、NodeManager、ResourceManager、NameNodedn01192.168.1.52DataNode、NodeManager、SecondaryNameNodedn02192.168.1.53DataNode、NodeManager2 、安…