【Rust测试】Rust代码测试方法详解与应用实战

在这里插入图片描述

✨✨ 欢迎大家来到景天科技苑✨✨

🎈🎈 养成好习惯,先赞后看哦~🎈🎈

🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。

所属的专栏:Rust语言通关之路
景天的主页:景天科技苑

在这里插入图片描述

Rust测试

测试是软件开发中不可或缺的重要环节,它能够确保代码质量、减少bug并提高软件的可靠性。
Rust语言从设计之初就内置了对测试的支持,提供了完善的测试框架和工具链。
本文将全面介绍Rust中的测试方法,从基础概念到高级技巧,并结合实际案例展示如何在Rust项目中实施有效的测试策略。

1. 如何编写测试

测试用来验证非测试的代码是否按照期望的方式运行的 Rust 函数。测试函数体通常执行如下三种操作:

  1. 设置任何所需的数据或状态
  2. 运行需要测试的代码
  3. 断言其结果是我们所期望的
    让我们看看 Rust 提供的专门用来编写测试的功能: test 属性、一些宏和 should_panic 属性。

2. Rust测试概述

1)Rust测试框架概述

Rust的测试框架是标准库的一部分,无需额外安装。主要包含以下组件:
#[test]属性:标记测试函数
assert!宏系列:用于断言
#[should_panic]:测试预期panic
#[ignore]:忽略某些测试
cargo test命令:运行测试

2)测试函数剖析

Rust 中的测试就是一个带有 test 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据:之前我们讲结构体中用到的 derive 属性就是一个例子。
为了将一个函数变成测试函数,需要在 fn 行之前加上 #[test] 。
当使用 cargo test 命令运行测试函数时,Rust 会构建一个测试执行者二进制文件 用来运行标记了 test 属性的函数 并报告每一个测试是通过还是失败。

当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。
这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。
当然也可以额外增加任意多的测试函数以及测试模块!

我们将先通过对自动生成的测试模板做一些试验来探索一些测试如何工作方面的内容,而不实际测试任何代码。
接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是否正确。

3)编写第一个测试

让我们从一个简单的例子开始:
让我们创建一个新的库项目 mytest01 :
在src下编写lib.rs

// src/lib.rs

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }
}

运行测试:

cargo test

在这里插入图片描述

Cargo 编译并运行了测试。在 Compiling 、 Finished 和 Running 这几行之后,可以看到 running 1 test 这一行。
下一行显示了生成的测试函数的名称,它是 it_works ,以及测试的运行结果, ok 。
接着可以看到全体测试运行结果的总结: test result: ok. 意味着所有测试都通过了。
1 passed; 0 failed 表示通过或失败的测试数量。
这里并没有任何被标记为忽略的测试,所以总结表明 0 ignored 。
我们也没有过滤需要运行的测试,所以总结的结尾显示 0 filtered out 。
0 measured 统计是针对性能测试的。
性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。

3. 断言宏详解

常见的断言宏
Rust提供了多种断言宏:

1)assert!: 检查布尔值为true

assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。
需要向 assert! 宏提供一个计算为布尔值的参数。如果值是 true , assert! 什么也不做同时测试会通过。
如果值为 false , assert! 调用 panic! 宏,这会导致测试失败。
assert! 宏帮助我们检查代码是否以期望的方式运行。

assert!(result.is_ok());
在这里插入图片描述

2)assert_eq!: 检查两个值相等

测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。
可以通过向 assert! 宏传递一个使用 == 运算符的表达式来做到。
不过这个操作实在是太常见了,以至于标注库提供了一对宏来更方便的处理这些操作: assert_eq! 和 assert_ne! 。
这两个宏分别比较两个值是相等还是不相等。
当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 为什么 失败,而 assert! 只会打印出它从 == 表达式中得到了 false 值,而不是导致false 的两个值。

assert_eq!(result.unwrap(), 42);

3)assert_ne!: 检查两个值不相等

assert_ne!(result, Err("invalid input"));

4)#[should_panic]: 测试预期panic

#[test]
#[should_panic(expected = "divide by zero")]
fn test_divide_by_zero() {
    divide(10, 0);
}

5)自定义错误

也可以向 assert! 、 assert_eq! 和 assert_ne! 宏传递一个可选的参数来增加用于打印的自定义错误信息。
任何在assert! 必需的一个参数和 assert_eq! 和 assert_ne! 必需的两个参数之后指定的参数都会传递给format! 宏,所以可以传递一个包含 {} 占位符的格式字符串和放入占位符的值。
自定义信息有助于记录断言的意义,这样到测试失败时,就能更好的理解代码出了什么问题。

例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
文件名: src/lib.rs

pub fn greeting(name: &str) -> String {
    format!("Hello {}!", name)
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_greeting() {
        let result = greeting("Alice");
        assert!(result.contains("Alice1"), "Greeting did not contain name, value was `{}`", result);
    }
}

名字传递不断,断言失败
在这里插入图片描述

4. 测试组织与策略

4.1 单元测试

单元测试通常放在与被测试代码相同的文件中:

// src/lib.rs

pub fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn internal() {
        assert_eq!(internal_adder(2, 2), 4);
    }
}

4.2 集成测试

集成测试位于项目根目录下的tests目录中:
my_project/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs

示例集成测试:

// tests/integration_test.rs

use my_project;

#[test]
fn test_add() {
    assert_eq!(my_project::add(2, 2), 4);
}

4.3 文档测试

Rust可以在文档注释中嵌入可执行的测试用例:

/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// use my_project::add;
///
/// assert_eq!(add(2, 2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

运行文档测试:

cargo test --doc

5. 常用测试工具和crate

5.1 常用测试crate

mockito:HTTP mocking
proptest:基于属性的测试
test-case:参数化测试
rstest:更灵活的测试框架
criterion:基准测试

5.2 使用proptest进行属性测试

首先在项目中添加protest包
cargo add proptest
在这里插入图片描述

// 在Cargo.toml中添加:
// [dev-dependencies]
// proptest = "1.0.0"

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod proptests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn test_add_commutative(a in 0..1000i32, b in 0..1000i32) {
            assert_eq!(add(a, b), add(b, a));
        }
        
        #[test]
        fn test_add_associative(a in 0..1000i32, b in 0..1000i32, c in 0..1000i32) {
            assert_eq!(add(add(a, b), c), add(a, add(b, c)));
        }
    }
}

运行测试
在这里插入图片描述

6. 控制测试如何运行

就像 cargo run 会编译代码并运行生成的二进制文件一样, cargo test 在测试模式下编译代码并运行生成的测试二进制文件。
可以指定命令行参数来改变 cargo test 的默认行为。
例如, cargo test 生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过程中产生的输出避免他们被显示出来,使得阅读测试结果相关的内容变得更容易。
这些选项的一部分可以传递给 cargo test ,而另一些则需要传递给生成的测试二进制文件。
为了分隔两种类型的参数,首先列出传递给 cargo test 的参数,接着是分隔符 – ,再之后是传递给测试二进制文件的参数。
运行 cargo test --help 会告诉你 cargo test 的相关参数,而运行 cargo test – --help 则会告诉你位于分隔符 – 之后的相关参数
注意:该命令是在项目中运行
在这里插入图片描述

6.1 并行或连续的运行测试

当运行多个测试时,他们默认使用线程来并行的运行。
这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。
因为测试是在同时运行的,你应该小心测试不能相互依赖或依赖任何共享状态,这包括类似于当前工作目录或者环境变量这样的共享环境。
例如,每一个测试都运行一些代码在硬盘上创建一个 test-output.txt 文件并写入一些数据。
接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。
因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。
那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。
一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。
如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递 --test-threads 参数和希望使用线程的数量给测试二进制文件。例如:

cargo test -- --test-threads=1

这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过测试就不会在存在共享状态时潜在的相互干涉了。

6.2 显示函数输出

如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。
例如,如果在测试中调用 println! 而测试通过了,我们将不会在终端看到 println! 的输出:只会看到说明测试通过的行。
如果测试失败了,就会看到所有标准输出和其他错误信息。

运行测试通过,不显示函数打印信息
在这里插入图片描述

如果测试,不通过,则显示函数打印信息
在这里插入图片描述

如果我们想要测试通过,也显示函数打印信息怎么办呢?
捕获输出的行为可以通过 --nocapture 参数来显示

cargo test -- --ignored --nocapture

在这里插入图片描述

6.3 只运行部分测试函数

有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这些代码相关的测试。
可以向cargo test 传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。
文件名: src/lib.rs

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 传递任意测试的名称来只运行这个测试:

cargo test one_hundred

在这里插入图片描述

只有名称为 one_hundred 的测试被运行了;其余两个测试并不匹配这个名称。测试输出在总结行的结尾显示了 2 filtered out 表明存在比本命令所运行的更多的测试。
不能像这样指定多个测试名称,只有传递给 cargo test 的第一个值才会被使用。不过有运行多个测试的方法。

过滤运行多个测试
然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含 add ,
可以通过 cargo test add 来运行这两个测试:
在这里插入图片描述

这运行了所有名字中带有 add 的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。

除非指定否则忽略某些测试
有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 cargo test 的时候希望能排除他们。
与其通过参数列举出所有希望运行的测试,也可以使用 ignore 属性来标记耗时的测试并排除他们,如下所示:

文件名: src/lib.rs

#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
    // code that takes an hour to run
}

对想要排除的测试的 #[test] 之后增加了 #[ignore] 行。现在如果运行测试,就会发现 it_works 运行了,而expensive_test 没有运行:
在这里插入图片描述

通过控制运行哪些测试,可以确保运行 cargo test 的结果是快速的。
当某个时刻需要检查 ignored 测试的结果而且你也有时间等待这个结果的话,可以选择执行

cargo test -- --ignored 

在这里插入图片描述

评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

景天科技苑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值