Update(test): unified format 3

pull/509/head
Allan Downey 3 years ago
parent 73d8f7df47
commit fa45f219a7

@ -1,14 +1,18 @@
# 断言assertion
# 断言 assertion
在编写测试函数时,断言决定了我们的测试是通过还是失败,它为结果代言。在前面,大家已经见识过 `assert_eq!` 的使用,下面一起来看看 Rust 为我们提供了哪些好用的断言。
## 断言列表
在正式开始前,来看看常用的断言有哪些:
- `assert!`, `assert_eq!`, `assert_ne!`, 它们会在所有模式下运行
- `debug_assert!`, `debug_assert_eq!`, `debug_assert_ne!`, 它们只会在 `Debug` 模式下运行
## assert_eq!
`assert_eq!` 宏可以用于判断两个表达式返回的值是否相等 :
```rust
fn main() {
let a = 3;
@ -18,6 +22,7 @@ fn main() {
```
当不相等时,当前线程会直接 `panic`:
```rust
fn main() {
let a = 3;
@ -27,6 +32,7 @@ fn main() {
```
运行后报错如下:
```shell
$ cargo run
thread 'main' panicked at 'assertion failed: `(left == right)`
@ -45,9 +51,11 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
**以上特征限制对于下面即将讲解的 `assert_ne!` 一样有效,** 就不再重复讲述。
## assert_ne!
`assert_ne!` 在使用和限制上与 `assert_eq!` 并无区别,唯一的区别就在于,前者判断的是两者的不相等性。
我们将之前报错的代码稍作修改:
```rust
fn main() {
let a = 3;
@ -59,7 +67,9 @@ fn main() {
由于 `a``b` 不相等,因此 `assert_ne!` 会顺利通过,不再报错。
## assert!
`assert!` 用于判断传入的布尔表达式是否为 `true`:
```rust
// 以下断言的错误信息只包含给定表达式的返回值
assert!(true);
@ -78,6 +88,7 @@ assert!(a + b == 30, "a = {}, b = {}", a, b);
```
来看看该如何使用 `assert!` 进行单元测试 :
```rust
#[derive(Debug)]
struct Rectangle {
@ -113,7 +124,9 @@ mod tests {
```
## `debug_assert!` 系列
`debug_assert!`, `debug_assert_eq!`, `debug_assert_ne!` 这三个在功能上与之前讲解的版本并无区别,主要区别在于,`debug_assert!` 系列只能在 `Debug` 模式下输出,例如如下代码:
```rust
fn main() {
let a = 3;
@ -123,6 +136,7 @@ fn main() {
```
`Debug` 模式下运行输出错误信息:
```shell
$ cargo run
thread 'main' panicked at 'assertion failed: `(left == right)`
@ -132,6 +146,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
但是在 `Release` 模式下却没有任何输出:
```shell
$ cargo run --release
```

@ -1,4 +1,5 @@
# 基准测试benchmark
# 基准测试 benchmark
几乎所有开发都知道,如果要测量程序的性能,就需要性能测试。
性能测试包含了两种:压力测试和基准测试。前者是针对接口 API模拟大量用户去访问接口然后生成接口级别的性能数据而后者是针对代码可以用来测试某一段代码的运行速度例如一个排序算法。
@ -10,26 +11,31 @@
事实上我们更推荐后者,原因在后文会详细介绍,下面先从官方提供的工具开始。
## 官方 benchmark
## 官方benchmark
官方提供的测试工具,目前最大的问题就是只能在非 `stable` 下使用,原因是需要在代码中引入 `test` 特性: `#![feature(test)]`
#### 设置 Rust 版本
因此在开始之前,我们需要先将当前仓库中的 [`Rust 版本`](https://course.rs/appendix/rust-version.html#不稳定功能)从 `stable` 切换为 `nightly`:
1. 安装 `nightly` 版本:`$ rustup install nightly`
2. 使用以下命令确认版本已经安装成功
```shell
$ rustup toolchain list
stable-aarch64-apple-darwin (default)
nightly-aarch64-apple-darwin (override)
```
3. 进入 `adder` 项目(之前为了学习测试专门创建的项目)的根目录,然后运行 `rustup override set nightly`,将该项目使用的 `rust` 设置为 `nightly`
很简单吧,其实只要一个命令就可以切换指定项目的 Rust 版本,例如你还能在基准测试后再使用 `rustup override set stable` 切换回 `stable` 版本。
#### 使用 benchmark
当完成版本切换后,就可以开始正式编写 `benchmark` 代码了。首先,将 `src/lib.rs` 中的内容替换成如下代码:
```rust
#![feature(test)]
@ -57,6 +63,7 @@ mod tests {
```
可以看出,`benchmark` 跟单元测试区别不大,最大的区别在于它是通过 `#[bench]` 标注,而单元测试是通过 `#[test]` 进行标注,这意味着 `cargo test` 将不会运行 `benchmark` 代码:
```shell
$ cargo test
running 2 tests
@ -69,6 +76,7 @@ test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
`cargo test` 直接把我们的 `benchmark` 代码当作单元测试处理了,因此没有任何性能测试的结果产生。
对此,需要使用 `cargo bench` 命令:
```shell
$ cargo bench
running 2 tests
@ -84,6 +92,7 @@ test result: ok. 0 passed; 0 failed; 1 ignored; 1 measured; 0 filtered out; fini
- benchmark 的结果是 `0 ns/iter`,表示每次迭代( `b.iter` )耗时 `0 ns`,奇怪,怎么是 `0` 纳秒呢?别急,原因后面会讲
#### 一些使用建议
关于 `benchmark`,这里有一些使用建议值得大家关注:
- 将初始化代码移动到 `b.iter` 循环之外,否则每次循环迭代都会初始化一次,这里只应该存放需要精准测试的代码
@ -92,7 +101,9 @@ test result: ok. 0 passed; 0 failed; 1 ignored; 1 measured; 0 filtered out; fini
- 循环内的代码应该尽量的短小快速,因为这样循环才能被尽可能多的执行,结果也会更加准确
#### 迷一般的性能结果
在写 `benchmark` 时,你可能会遇到一些很纳闷的棘手问题,例如以下代码:
```rust
#![feature(test)]
@ -138,11 +149,13 @@ mod tests {
}
}
```
通过`cargo bench`运行后,得到一个难以置信的结果:`test tests::bench_u64 ... bench: 0 ns/iter (+/- 0)`, 难道Rust已经到达量子计算机级别了
通过`cargo bench`运行后,得到一个难以置信的结果:`test tests::bench_u64 ... bench: 0 ns/iter (+/- 0)`, 难道 Rust 已经到达量子计算机级别了?
其实,原因藏在`LLVM`中: `LLVM`认为`fibonacci_u64`函数调用的结果没有使用,同时也认为该函数没有任何副作用(造成其它的影响,例如修改外部变量、访问网络等), 因此它有理由把这个函数调用优化掉!
解决很简单,使用 Rust 标准库中的 `black_box` 函数:
```rust
for i in 100..200 {
test::black_box(fibonacci_u64(test::black_box(i)));
@ -162,8 +175,8 @@ test result: ok. 0 passed; 0 failed; 1 ignored; 1 measured; 0 filtered out; fini
嗯,这次结果就明显正常了。
## criterion.rs
官方 `benchmark` 有两个问题,首先就是不支持 `stable` 版本的 Rust其次是结果有些简单缺少更详细的统计分布。
因此社区 `benchmark` 就应运而生,其中最有名的就是 [`criterion.rs`](https://github.com/bheisler/criterion.rs),它有几个重要特性:
@ -172,6 +185,7 @@ test result: ok. 0 passed; 0 failed; 1 ignored; 1 measured; 0 filtered out; fini
- 图表,使用 [`gnuplots`](http://www.gnuplot.info) 展示详细的结果图表
首先,如果你需要图表,需要先安装 `gnuplots`,其次,我们需要引入相关的包,在 `Cargo.toml` 文件中新增 :
```toml
[dev-dependencies]
criterion = "0.3"
@ -182,6 +196,7 @@ harness = false
```
接着,在项目中创建一个测试文件: `$PROJECT/benches/my_benchmark.rs`,然后加入以下内容:
```rust
use criterion::{black_box, criterion_group, criterion_main, Criterion};
@ -202,6 +217,7 @@ criterion_main!(benches);
```
最后,使用 `cargo bench` 运行并观察结果:
```shell
Running target/release/deps/example-423eedc43b2b3a93
Benchmarking fib 20
@ -218,5 +234,3 @@ median [25.733 us 25.988 us] med. abs. dev. [234.09 ns 544.07 ns]
```
可以看出,这个结果是明显比官方的更详尽的,如果大家希望更深入的学习它的使用,可以参见[官方文档](https://bheisler.github.io/criterion.rs/book/getting_started.html)。

@ -1,4 +1,5 @@
# 用Github Actions进行持续集成
# 用 Github Actions 进行持续集成
[Github Actions](https://github.com/features/actions) 是官方于 2018 年推出的持续集成服务,它非常强大,本文将手把手带领大家学习如何使用 `Github Actions` 对 Rust 项目进行持续集成。
持续集成是软件开发中异常重要的一环,大家应该都听说过 `Jenkins`,它就是一个拥有悠久历史的持续集成工具。简单来说,持续集成会定期拉取同一个项目中所有成员的相关代码,对其进行自动化构建。
@ -8,6 +9,7 @@
在有了持续集成后,只要编写好相应的编译、测试、发布配置文件,那持续集成平台会自动帮助我们完成整个相关的流程,期间无需任何人介入,高效且可靠。
## Github Actions
而本文的主角正是这样的持续集成平台,它由 Github 官方提供,并且跟 github 进行了深度的整合,其中 `actions` 代表了代码拉取、测试运行、登陆远程服务器、发布到第三方服务等操作行为。
最妙的是 Github 发现这些 `actions` 其实在很多项目中都是类似的,意味着 `actions` 完全可以被多个项目共享使用,而不是每个项目都从零开发自己的 `actions`
@ -15,6 +17,7 @@
若你需要某个 `action`,不必自己写复杂的脚本,直接引用他人写好的 `action` 即可,整个持续集成过程,就变成了多个 `action` 的组合,这就是` GitHub Actions` 最厉害的地方。
#### action 的分享与引用
既然 `action` 这么强大,我们就可以将自己的 `action` 分享给他人,也可以引用他人分享的 `action`,有以下几种方式:
1. 将你的 `action` 放在 github 上的公共仓库中,这样其它开发者就可以引用,例如 [github-profile-summary-cards](https://github.com/vn7n24fzkq/github-profile-summary-cards) 就提供了相应的 `action`,可以生成 github 用户统计信息,然后嵌入到你的个人主页中,具体效果[见这里](https://github.com/sunface)
@ -25,6 +28,7 @@
对于第一点这里再补充下,如果你想要引用某个代码仓库中的 `action` ,可以通过 `userName/repoName` 方式来引用: 例如你可以通过 `actions/setup-node` 来引用 `github.com/actions/setup-node` 仓库中的 `action`,该 `action` 的作用是安装 Node.js。
由于 `action` 是代码仓库,因此就有版本的概念,你可以使用 `@` 符号来引入同一个仓库中不同版本的 `action`,例如:
```yml
actions/setup-node@master # 指向一个分支
actions/setup-node@v2.5.1 # 指向一个 release
@ -34,14 +38,17 @@ actions/setup-node@f099707 # 指向一个 commit
如果希望深入了解,可以进一步查看官方的[文档](https://docs.github.com/cn/actions/creating-actions/about-custom-actions#using-release-management-for-actions)。
## Actions 基础
在了解了何为 Github Actions 后,再来通过一个基本的例子来学习下它的基本概念,注意,由于篇幅有限,我们只会讲解最常用的部分,如果想要完整的学习,请移步[这里](https://docs.github.com/en/actions)。
#### 创建 action demo
首先,为了演示,我们需要创建一个公开的 github 仓库 `rust-action`,然后在仓库主页的导航栏中点击 `Actions` ,你会看到如下页面 :
<img src="https://pic1.zhimg.com/80/v2-4bb58f042c7a285219910bfd3c259464_1440w.jpg" />
接着点击 `set up a workflow yourself ->` ,你将看到系统为你自动创建的一个工作流 workflow ,在 `rust-action/.github/workflows/main.yml` 文件中包含以下内容:
```yml
# 下面是一个基础的工作流,你可以基于它来编写自己的 Github Actions
name: CI
@ -50,9 +57,9 @@ name: CI
on:
# 当 `push``pull request` 事件发生时就触发工作流的执行,这里仅仅针对 `main` 分支
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
# 允许用于在 `Actions` 标签页中手动运行工作流
workflow_dispatch:
@ -83,6 +90,7 @@ jobs:
```
#### 查看工作流信息
通过内容的注释,大家应该能大概理解这个工作流是怎么回事了,在具体讲解前,我们先完成 `Actions` 的创建,点击右上角的 `Start Commit` 绿色按钮提交,然后再回到 `Actions` 标签页,你可以看到如下界面:
<img src="https://pic2.zhimg.com/80/v2-301a8feac57633f34f9cd638ac139c22_1440w.jpg" />
@ -93,7 +101,6 @@ jobs:
<img src="https://pic3.zhimg.com/80/v2-99fb593bc3140f71c316ce0ba6249911_1440w.png"/>
还记得之前配置中的 `workflow_dispatch` 嘛?它允许工作流被手动执行:点击左边的 `All workflows -> CI` ,可以看到如下页面。
<img src="https://pic3.zhimg.com/80/v2-cc1d9418f6befb5a089cde659666e65e_1440w.png" />
@ -109,6 +116,7 @@ jobs:
至此,我们已经初步掌握 `Github Actions` 的用法,现在来看看一些基本的概念。
#### 基本概念
- **Github Actions**,每个项目都拥有一个 `Actions` ,可以包含多个工作流
- **workflow 工作流**,描述了一次持续集成的过程
- **job 作业**,一个工作流可以包含多个作业,因为一次持续集成本身就由多个不同的部分组成
@ -117,22 +125,25 @@ jobs:
可以看出,每一个概念都是相互包含的关系,前者包含了后者,层层相扣,正因为这些精心设计的对象才有了强大的 `Github Actions`
#### on
`on` 可以设定事件用于触发工作流的运行:
1. 一个或多个 Github 事件,例如 `push` 一个 `commit`、创建一个 `issue`、提交一次 `pr` 等等,详细的事件列表参见[这里](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows)
2. 预定的时间,例如每天零点零分触发,详情见[这里](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule)
```yml
on:
schedule:
-cron:'00 ***'
schedule: -cron:'00 ***'
```
3. 外部事件触发,例如你可以通过 `REST API` 向 Github 发送请求去触发,具体请查阅[官方文档](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository_dispatch)
#### jobs
工作流由一个或多个作业( job )组成,这些作业可以顺序运行也可以并行运行,同时我们还能使用 `needs` 来指定作业之间的依赖关系:
```yml
jobs:
job1:
@ -147,6 +158,7 @@ jobs:
这里的 `job2` 必须等待 `job1` 成功后才能运行,而 `job3` 则需要等待 `job1``job2`
#### runs-on
指定作业的运行环境,运行器 `runner` 分为两种:`GitHub-hosted runner` 和 `self-hosted runner`,后者是使用自己的机器来运行作业,但是需要 Github 能进行访问并给予相应的机器权限,感兴趣的同学可以看看[这里](https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job#choosing-self-hosted-runners)。
而对于前者Github 提供了以下的运行环境:
@ -156,7 +168,9 @@ jobs:
其中比较常用的就是 `runs-on:ubuntu-latest`
#### strategy.matrix
有时候我们常常需要对多个操作系统、多个平台、多个编程语言版本进行测试,为此我们可以配置一个 `matrix` 矩阵:
```yml
runs-on: ${{ matrix.os }}
strategy:
@ -174,13 +188,16 @@ steps:
当然,`matrix` 能做的远不止这些,例如,你还可以定义自己想要的 `kv` 键值对,想要深入学习的话可以看看[官方文档](https://docs.github.com/en/actions/using-jobs/using-a-build-matrix-for-your-jobs)。
#### strategy
除了 `matrix` `strategy` 中还能设定以下内容:
- `fail-fast` : 默认为true ,即一旦某个矩阵任务失败则立即取消所有还在进行中的任务
- `fail-fast` : 默认为 true ,即一旦某个矩阵任务失败则立即取消所有还在进行中的任务
- `max-paraller` : 可同时执行的最大并发数,默认情况下 GitHub 会动态调整
#### env
用于设定环境变量,可以用于以下地方:
- env
- jobs.<job_id>.env
@ -204,10 +221,10 @@ jobs:
如果有多个 `env` 存在,会使用就近那个。
至此,`Github Actions` 的常用内容大家已经基本了解,下面来看一个实用的示例。
## 真实示例:生成 Github 统计卡片
相信大家看过不少用户都定制了自己的个性化 Github 首页,这个是通过在个人名下创建一个同名的仓库来实现的,该仓库中的 `Readme.md` 的内容会自动展示在你的个人首页中,例如 `Sunface` 的[个人首页](https://github.com/sunface) 和内容所在的[仓库](https://github.com/sunface/sunface)。
大家可能会好奇上面链接中的 Github 统计卡片如何生成,其实有两种办法:
@ -218,6 +235,7 @@ jobs:
第一种的优点就是非常简单,缺点是样式不太容易统一,不能对齐对于强迫症来说实在难受 :( 而后者的优点是规规整整的卡片,缺点就是使用起来更加复杂,而我们正好借此来看看真实的 `Github Actions` 长什么样。
首先,在你的同名项目下创建 `.github/workflows/profile-summary-cards.yml` 文件,然后填入以下内容:
```yml
# 工作流名称
name: GitHub-Profile-Summary-Cards
@ -248,15 +266,16 @@ jobs:
当提交后,该工作流会自动在当前项目中生成 `profile-summary-card-output` 目录,然后将所有卡片放入其中,当然我们这里使用了定时触发的机制,并没有基于 `pr` 或`push` 来触发,如果你在编写过程中,希望手动触发来看看结果,请参考前文的手动触发方式。
这里我们引用了 `vn7n24fzkq/github-profile-summary-cards@release``action`,位于 `https://github.com/vn7n24fzkq/github-profile-summary-cards` 仓库中,并指定使用 `release` 分支。
接下来就可以愉快的[使用这些卡片](https://github.com/sunface/sunface/edit/master/Readme.md)来定制我们的主页了: )
## 使用 Actions 来构建 Rust 项目
其实 Rust 项目也没有什么特别之处,我们只需要在 `steps` 逐步构建即可,下面给出该如何测试和构建的示例。
#### 测试
```yml
on: [push, pull_request]
@ -324,7 +343,6 @@ jobs:
args: -- -D warnings
```
## 构建
```yml
@ -398,3 +416,4 @@ jobs:
```
限于文章篇幅有限,我们就不再多做解释,大家有疑问可以看看文中给出的文档链接,顺便说一句官方文档是支持中文的!

@ -9,4 +9,3 @@ Rust 语言本身就非常关注安全性,但是语言级别的安全性并不
例如,假设我们有一个函数 `add_two` 用于将两个整数进行相加并返回一个整数结果。没错Rust 的类型系统可以通过函数签名确保我们的输入和输出类型都是正确的,譬如你无法传入一个字符串作为输入,但是 Rust 无法保证函数中代码逻辑的正确性:明明目标是相加操作,却给整成了 `x - y`
好在,写测试可以解决类似的问题。但也不要迷信测试,文章开头的那句话说明一切。

@ -1,10 +1,13 @@
# 单元测试、集成测试
在了解了如何在 Rust 中写测试用例后,本章节我们将学习如何实现单元测试、集成测试,其实它们用到的技术还是[上一章节](https://course.rs/test/write-tests.html)中的测试技术,只不过对如何组织测试代码提出了新的要求。
## 单元测试
单元测试目标是测试某一个代码单元(一般都是函数),验证该单元是否能按照预期进行工作,例如测试一个 `add` 函数,验证当给予两个输入时,最终返回的和是否符合预期。
在 Rust 中,单元测试的惯例是将测试代码的模块跟待测试的正常代码放入同一个文件中,例如 `src/lib.rs` 文件中有如下代码:
```rust
pub fn add_two(a: i32) -> i32 {
a + 2
@ -24,6 +27,7 @@ mod tests {
`add_two` 是我们的项目代码,为了对它进行测试,我们在同一个文件中编写了测试模块 `tests`,并使用 `#[cfg(test)]` 进行了标注。
#### 条件编译 `#[cfg(test)]`
上面代码中的 `#[cfg(test)]` 标注可以告诉 Rust 只有在 `cargo test` 时才编译和运行模块 `tests`,其它时候当这段代码是空气即可,例如在 `cargo build` 时。这么做有几个好处:
- 节省构建代码时的编译时间
@ -36,7 +40,9 @@ mod tests {
大家看出来了吗?这是典型的条件编译,`Cargo` 会根据指定的配置来选择是否编译指定的代码,事实上关于条件编译 Rust 能做的不仅仅是这些,在 [`Cargo` 专题](https://course.rs/cargo/intro.html)中我们会进行更为详细的介绍。
#### 测试私有函数
关于私有函数能否被直接测试,编程社区里一直争论不休,甚至于部分语言可能都不支持对私有函数进行测试或者难以测试。无论你的立场如何,反正 Rust 是支持对私有函数进行测试的:
```rust
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
@ -62,16 +68,19 @@ mod tests {
但是在上述代码中,我们使用 `use super::*;``tests` 的父模块中的所有内容引入到当前作用域中,这样就可以非常简单的实现对私有函数的测试。
## 集成测试
与单元测试的同吃同住不同,集成测试的代码是在一个单独的目录下的。由于它们使用跟其它模块一样的方式去调用你想要测试的代码,因此只能调用通过 `pub` 定义的 `API`,这一点与单元测试有很大的不同。
如果说单元测试是对代码单元进行测试,那集成测试则是对某一个功能或者接口进行测试,因此单元测试的通过,并不意味着集成测试就能通过:局部上反映不出的问题,在全局上很可能会暴露出来。
#### *tests* 目录
#### _tests_ 目录
一个标准的 Rust 项目,在它的根目录下会有一个 `tests` 目录,大名鼎鼎的 [`ripgrep`](https://github.com/BurntSushi/ripgrep) 也不能免俗。
没错该目录就是用来存放集成测试的Cargo 会自动来此目录下寻找集成测试文件。我们可以在该目录下创建任何文件Cargo 会对每个文件都进行自动编译,但友情提示下,最好按照合适的逻辑来组织你的测试代码。
首先来创建一个集成测试文件 `tests/integration_test.rs` ,注意,`tests` 目录一般来说需要手动创建,该目录在项目的根目录下,跟 `src` 目录同级。然后在文件中填入如下测试代码:
```rust
use adder;
@ -86,6 +95,7 @@ fn it_adds_two() {
首先与单元测试有所不同,我们并没有创建测试模块。其次,`tests` 目录下的每个文件都是一个单独的包,我们需要将待测试的包引入到当前包的作用域后: `use adder`,才能进行测试 。大家应该还记得[包和模块章节](https://course.rs/advance/crate-module/crate.html)中讲过的内容吧?在创建项目后,`src/lib.rs` 自动创建一个与项目同名的 `lib` 类型的包,由于我们的项目名是 `adder`,因此包名也是 `adder`
因为 `tests` 目录本身就说明了它的特殊用途,因此我们无需再使用 `#[cfg(test)]` 来取悦 Cargo。后者会在运行 `cargo test` 时,对 `tests` 目录中的每个文件都进行编译运行。
```shell
$ cargo test
Running unittests (target/debug/deps/adder-8a400aa2b5212836)
@ -114,6 +124,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
首先是单元测试被运行 `Running unittests` ,其次就是我们的主角集成测试的运行 `Running tests/integration_test.rs`,可以看出,集成测试的输出内容与单元测试并没有大的区别。最后运行的是文档测试 `Doc-tests adder`
与单元测试类似,我们可以通过[指定名称的方式](https://course.rs/test/write-tests.html#指定运行一部分测试)来运行特定的集成测试用例:
```shell
$ cargo test --test integration_test
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
@ -129,9 +140,11 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
大家可以尝试下在同一个测试文件中添加更多的测试用例或者添加更多的测试文件,并观察测试输出会如何变化。
#### 共享模块
在集成测试的 `tests` 目录下,每一个文件都是一个独立的包,这种组织方式可以很好的帮助我们理清测试代码的关系,但是如果大家想要在多个文件中共享同一个功能该怎么做?例如函数 `setup` 可以用于状态初始化,然后多个测试包都需要使用该函数进行状态的初始化。
也许你会想要创建一个 `tests/common.rs` 文件,然后将 `setup` 函数放入其中:
```rust
pub fn setup() {
// 初始化一些测试状态
@ -140,6 +153,7 @@ pub fn setup() {
```
但是当我们运行 `cargo test` 后,会发现该函数被当作集成测试函数运行了,即使它并没有包含任何测试功能,也没有被其它测试文件所调用:
```shell
$ cargo test
Running tests/common.rs (target/debug/deps/common-5c21f4f2c87696fb)
@ -168,6 +182,7 @@ fn it_adds_two() {
此时,就可以在测试中调用 `common` 中的共享函数了,不过还有一点值得注意,为了使用 `common`,这里使用了 `mod common` 的方式来声明该模块。
#### 二进制包的集成测试
目前来说Rust 只支持对 `lib` 类型的包进行集成测试,对于二进制包例如 `src/main.rs` 是无能为力的。原因在于,我们无法在其它包中使用 `use` 引入二进制包,而只有 `lib` 类型的包才能被引入,例如 `src/lib.rs`
这就是为何我们需要将代码逻辑从 `src/main.rs` 剥离出去放入 `lib` 包中,例如很多 Rust 项目中都同时有 `src/main.rs``src/lib.rs` ,前者中只保留代码的主体脉络部分,而具体的实现通通放在类似后者的 `lib` 包中。
@ -175,6 +190,7 @@ fn it_adds_two() {
这样,我们就可以对 `lib` 包中的具体实现进行集成测试,由于 `main.rs` 中的主体脉络足够简单,当集成测试通过时,意味着 `main.rs` 中相应的调用代码也将正常运行。
## 总结
Rust 提供了单元测试和集成测试两种方式来帮助我们组织测试代码以解决代码正确性问题。
单元测试针对的是具体的代码单元,例如函数,而集成测试往往针对的是一个功能或接口 API正因为目标上的不同导致了两者在组织方式上的不同

@ -9,14 +9,17 @@
让我们来看看该如何使用 Rust 提供的特性来按照上述步骤编写测试用例。
## 测试函数
当使用 `Cargo` 创建一个 `lib` 类型的包时,它会为我们自动生成一个测试模块。先来创建一个 `lib` 类型的 `adder` 包:
```shell
$ cargo new adder --lib
Created library `adder` project
$ cd adder
```
创建成功后,在 *src/lib.rs* 文件中可以发现如下代码:
创建成功后,在 _src/lib.rs_ 文件中可以发现如下代码:
```rust
#[cfg(test)]
mod tests {
@ -36,10 +39,13 @@ mod tests {
换而言之,正是因为测试模块既可以定义测试函数又可以定义非测试函数,导致了我们必须提供一个特殊的标记 `test`,用于告知哪个函数才是测试函数。
#### assert_eq
在测试函数中,还使用到了一个内置的断言:`assert_eq`,该宏用于对结果进行断言:`2 + 2` 是否等于 `4`。与之类似Rust 还内置了其它一些实用的断言,具体参见[后续章节](https://course.rs/test/assertion.html)。
## cargo test
下面使用 `cargo test` 命令来运行项目中的所有测试:
```shell
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
@ -59,6 +65,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
```
上面测试输出中,有几点值得注意:
- 测试用例是分批执行的,`running 1 test` 表示下面的输出 `test result` 来自一个测试用例的运行结果。
- `test tests::it_works` 中包含了测试用例的名称
- `test result: ok` 中的 `ok` 表示测试成功通过
@ -71,7 +78,9 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
大家还可以尝试修改下测试函数的名称,例如修改为 `exploration`,看看运行结果将如何变化。
#### 失败的测试用例
是时候开始写自己的测试函数了,为了演示,这次我们来写一个会运行失败的:
```rust
#[cfg(test)]
mod tests {
@ -88,6 +97,7 @@ mod tests {
```
新的测试函数 `another` 相当简单粗暴,直接使用 `panic` 来报错,使用 `cargo test` 运行看看结果:
```shell
running 2 tests
test tests::another ... FAILED
@ -115,7 +125,9 @@ error: test failed, to rerun pass '--lib'
事实上,多线程运行测试虽然性能高,但是存在数据竞争的风险,在后文我们会对其进行详细介绍并给出解决方案。
## 自定义失败信息
默认的失败信息在有时候并不是我们想要的,来看一个例子:
```rust
pub fn greeting(name: &str) -> String {
format!("Hello {}!", name)
@ -134,6 +146,7 @@ mod tests {
```
使用 `cargo test` 运行后,错误如下:
```shell
test tests::greeting_contains_name ... FAILED
@ -149,6 +162,7 @@ failures:
```
可以看出,这段报错除了告诉我们错误发生的地方,并没有更多的信息,那再来看看该如何提供一些更有用的信息:
```rust
fn greeting_contains_name() {
let result = greeting("Sunface");
@ -163,6 +177,7 @@ fn greeting_contains_name() {
```
这段代码跟之前并无不同,只是为 `assert!` 新增了几个格式化参数,这种使用方式与 `format!` 并无区别。再次运行后,输出如下:
```shell
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at '你的问候中并没有包含目标姓名 孙飞 ,你的问候是 `Hello Sunface!`', src/lib.rs:14:9
@ -172,9 +187,11 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
这次的报错就清晰太多了,真棒!在测试用例少的时候,也许这种信息还无法体现最大的价值,但是一旦测试多了后,详尽的报错信息将帮助我们更好的进行 Debug。
## 测试 panic
在之前的例子中,我们通过 `panic` 来触发报错,但是如果一个函数本来就会 `panic` ,而我们想要检查这种结果呢?
也就是说,我们需要一个办法来测试一个函数是否会 `panic`,对此, Rust 提供了 `should_panic` 属性注解,和 `test` 注解一样,对目标测试函数进行标注即可:
```rust
pub struct Guess {
value: i32,
@ -212,6 +229,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
```
从输出可以看出, `panic` 的结果被准确的进行了测试,那如果测试函数中的代码不再 `panic` 呢?例如:
```rust
fn greater_than_100() {
Guess::new(50);
@ -219,6 +237,7 @@ fn greater_than_100() {
```
此时显然会测试失败,因为我们预期一个 `panic`,但是 `new` 函数顺利的返回了一个 `Guess` 实例:
```shell
running 1 test
test tests::greater_than_100 - should panic ... FAILED
@ -230,9 +249,11 @@ note: test did not panic as expected // 测试并没有按照预期发生 panic
```
#### expected
虽然 `panic` 被成功测试到,但是如果代码发生的 `panic` 和我们预期的 `panic` 不符合呢?因为一段糟糕的代码可能会在不同的代码行生成不同的 `panic`
鉴于此,我们可以使用可选的参数 `expected` 来说明预期的 `panic` 长啥样:
```rust
// --snip--
impl Guess {
@ -272,6 +293,7 @@ mod tests {
这里由于篇幅有限,我们就不再展示测试失败的报错,大家可以自己修改下 `expected` 的信息,然后看看报错后的输出长啥样。
## 使用 `Result<T, E>`
在之前的例子中,`panic` 扫清一切障碍,但是它也不是万能的,例如你想在测试中使用 `?` 操作符进行链式调用该怎么办?那就得请出 `Result<T, E>` 了:
```rust
@ -293,6 +315,7 @@ mod tests {
至此,关于如何写测试的基本知识,大家已经了解的差不多了,下面来看看该如何控制测试的执行。
## 使用 `--` 分割命令行参数
大家应该都知道 `cargo build` 可以将代码编译成一个可执行文件,那你知道 `cargo run``cargo test` 是如何运行的吗?其实道理都一样,这两个也是将代码编译成可执行文件,然后进行运行,唯一的区别就在于这个可执行文件随后会被删除。
正因为如此,`cargo test` 也可以通过命令行参数来控制测试的执行,例如你可以通过参数来让默认的多线程测试变成单线程下的测试。需要注意的是命令行参数有两种,这两种通过 `--` 进行分割:
@ -305,6 +328,7 @@ mod tests {
先来看看第二种参数中的其中一个,它可以控制测试是并行运行还是顺序运行。
## 测试用例的并行或顺序执行
当运行多个测试函数时,默认情况下是为每个测试都生成一个线程,然后通过主线程来等待它们的完成和结果。这种模式的优点很明显,那就是并行运行会让整体测试时间变短很多,运行过大量测试用例的同学都明白并行测试的重要性:生命苦短,我用并行。
但是有利就有弊,并行测试最大的问题就在于共享状态的修改,因为你难以控制测试的运行顺序,因此如果多个测试共享一个数据,那么对该数据的使用也将变得不可控制。
@ -312,6 +336,7 @@ mod tests {
例如,我们有多个测试,它们每个都会往该文件中写入一些**自己的数据**,最后再从文件中读取这些数据进行对比。由于所有测试都是同时运行的,当测试 `A` 写入数据准备读取并对比时,很有可能会被测试 `B` 写入新的数据,导致 `A` 写入的数据被覆盖,然后 `A` 再读取到的就是 `B` 写入的数据。结果 `A` 测试就会失败,而且这种失败还不是因为测试代码不正确导致的!
解决办法也有,我们可以让每个测试写入自己独立的文件中,当然,也可以让所有测试一个接着一个顺序运行:
```rust
$ cargo test -- --test-threads=1
```
@ -319,7 +344,9 @@ $ cargo test -- --test-threads=1
首先能注意到的是该命令行参数是第二种类型:提供给编译后的可执行文件的,因为它在 `--` 之后进行传递。其次,细心的同学可能会想到,线程数不仅仅可以指定为 `1`,还可以指定为 `4`、`8`,当然,想要顺序运行,就必须是 `1`
## 测试函数中的 `println!`
默认情况下,如果测试通过,那写入标准输出的内容是不会显示在测试结果中的:
```rust
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {}", a);
@ -345,6 +372,7 @@ mod tests {
```
上面代码使用 `println!` 输出收到的参数值,来看看测试结果:
```shell
running 2 tests
test tests::this_test_will_fail ... FAILED
@ -375,6 +403,7 @@ $ cargo test -- --show-output
如上所示,只需要增加一个参数,具体的输出就不再展示,总之这次大家一定可以顺利看到 `I got the value 4` 的身影。
## 指定运行一部分测试
在 Mysql 中有上百万的单元测试,如果使用类似 `cargo test` 的命令来运行全部的测试,那开发真的工作十分钟,吹牛八小时了。对于 Rust 的中大型项目也一样,每次都运行全部测试是不可接受的,特别是你的工作仅仅是项目中的一部分时。
```rust
@ -404,6 +433,7 @@ mod tests {
```
如果直接使用 `cargo test` 运行,那三个测试函数会同时并行的运行:
```shell
running 3 tests
test tests::add_three_and_two ... ok
@ -421,9 +451,10 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
就不说上百万测试,就说几百个,想象一下结果会是怎么样,下面我们来看看该如何解决这个问题。
#### 运行单个测试
这个很简单,只需要将指定的测试函数名作为参数即可:
```shell
$ cargo test one_hundred
running 1 test
@ -435,6 +466,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; fini
此时,只有测试函数 `one_hundred` 会被运行其它两个由于名称不匹配会被直接忽略。同时在上面的输出中Rust 也通过 `2 filtered out` 提示我们:有两个测试函数被过滤了。
但是,如果你试图同时指定多个名称,那抱歉:
```shell
$ cargo test one_hundred,add_two_and_two
$ cargo test one_hundred add_two_and_two
@ -443,7 +475,9 @@ $ cargo test one_hundred add_two_and_two
这两种方式统统不行,此时就需要使用名称过滤的方式来实现了。
#### 通过名称来过滤测试
我们可以通过指定部分名称的方式来过滤运行相应的测试:
```shell
$ cargo test add
running 2 tests
@ -454,6 +488,7 @@ test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; fini
```
事实上,你不仅可以使用前缀,还能使用名称中间的一部分:
```shell
$ cargo test and
running 2 tests
@ -464,6 +499,7 @@ test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; fini
```
其中还有一点值得注意,那就是测试模块 `tests` 的名称也出现在了最终结果中:`tests::add_two_and_two`,这是非常贴心的细节,也意味着我们可以通过**模块名称来过滤测试**
```shell
cargo test tests
running 3 tests
@ -475,7 +511,9 @@ test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
```
#### 忽略部分测试
有时候,一些测试会非常耗时间,因此我们希望在 `cargo test` 中对它进行忽略,如果使用之前的方式,我们需要将所有需要运行的名称指定一遍,这非常麻烦,好在 Rust 允许通过 `ignore` 关键字来忽略特定的测试用例:
```rust
#[test]
fn it_works() {
@ -490,6 +528,7 @@ fn expensive_test() {
```
在这里,我们使用 `#[ignore]``expensive_test` 函数进行了标注,看看结果:
```shell
$ cargo test
running 2 tests
@ -508,6 +547,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
输出中的 `test expensive_test ... ignored` 意味着该测试函数被忽略了,因此并没有被执行。
当然,也可以通过以下方式运行被忽略的测试函数:
```shell
$ cargo test -- --ignored
running 1 test
@ -523,7 +563,9 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
```
#### 组合过滤
上面的方式虽然很强大,但是单独使用依然存在局限性。好在它们还能组合使用,例如还是之前的代码:
```rust
#[cfg(test)]
mod tests {
@ -549,6 +591,7 @@ mod tests {
```
然后运行 `tests` 模块中的被忽略的测试函数
```shell
$ cargo test tests -- --ignored
running 2 tests
@ -559,6 +602,7 @@ test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; fini
```
运行名称中带 `run` 且被忽略的测试函数:
```shell
$ cargo test run -- --ignored
running 1 test
@ -569,13 +613,14 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; fini
类似的还有很多,大家可以自己摸索研究下,总之,熟练掌握测试的使用是非常重要的,虽然包括我在内的很多开发并不喜欢写测试 :)
## `[dev-dependencies]`
`package.json`( Nodejs )文件中的 `devDependencies` 一样, Rust 也能引入只在开发测试场景使用的外部依赖。
其中一个例子就是 [`pretty_assertions`](https://docs.rs/pretty_assertions/1.0.0/pretty_assertions/index.html),它可以用来扩展标准库中的 `assert_eq!``assert_ne!`,例如提供彩色字体的结果对比。
`Cargo.toml` 文件中添加以下内容来引入 `pretty_assertions`
```toml
# standard crate data is left out
[dev-dependencies]
@ -583,6 +628,7 @@ pretty_assertions = "1"
```
然后在 `src/lib.rs` 中添加:
```rust
pub fn add(a: i32, b: i32) -> i32 {
a + b
@ -602,11 +648,12 @@ mod tests {
`tests` 模块中,我们通过 `use pretty_assertions::assert_eq;` 成功的引入之前添加的包,由于 `tests` 模块明确的用于测试目的,这种引入并不会报错。 大家可以试试在正常代码(非测试代码)中引入该包,看看会发生什么。
## 生成测试二进制文件
在有些时候,我们可能希望将测试与别人分享,这种情况下生成一个类似 `cargo build` 的可执行二进制文件是很好的选择。
事实上,在 `cargo test` 运行的时候,系统会自动为我们生成一个可运行测试的二进制可执行文件:
```shell
$ cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.00s
@ -614,6 +661,7 @@ $ cargo test
```
这里的 `target/debug/deps/study_cargo-0d693f72a0f49166` 就是可执行文件的路径和名称,我们直接运行该文件来执行编译好的测试:
```shell
$ target/debug/deps/study_cargo-0d693f72a0f49166
@ -626,3 +674,4 @@ test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
```
如果你只想生成编译生成文件,不想看 `cargo test` 的输出结果,还可以使用 `cargo test --no-run`.

Loading…
Cancel
Save