mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 00:43:59 +08:00
commit
5c9925a994
@ -18,4 +18,4 @@
|
|||||||
|
|
||||||
我们可以编写测试断言,比如说,当传递 `3` 给 `add_two` 函数时,返回值是 `5`。无论何时对代码进行修改,都可以运行测试来确保任何现存的正确行为没有被改变。
|
我们可以编写测试断言,比如说,当传递 `3` 给 `add_two` 函数时,返回值是 `5`。无论何时对代码进行修改,都可以运行测试来确保任何现存的正确行为没有被改变。
|
||||||
|
|
||||||
测试是一项复杂的技能:虽然不能在一本书的一个章节中就涉及到编写好的测试的所有细节,我们还是会讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
|
测试是一项复杂的技能:虽然不能在一个章节的篇幅中介绍如何编写好的测试的每个细节,但我们还是会讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 4464eab0892297b83db7134b7ace12762a89b389
|
> commit 4464eab0892297b83db7134b7ace12762a89b389
|
||||||
|
|
||||||
测试用来验证非测试的代码是否按照期望的方式运行的 Rust 函数。测试函数体通常执行如下三种操作:
|
Rust 中的测试函数是用来验证非测试代码是否按照期望的方式运行的。测试函数体通常执行如下三种操作:
|
||||||
|
|
||||||
1. 设置任何所需的数据或状态
|
1. 设置任何所需的数据或状态
|
||||||
2. 运行需要测试的代码
|
2. 运行需要测试的代码
|
||||||
@ -15,11 +15,11 @@
|
|||||||
|
|
||||||
### 测试函数剖析
|
### 测试函数剖析
|
||||||
|
|
||||||
作为最简单例子,Rust 中的测试就是一个带有 `test` 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据:第五章中结构体中用到的 `derive` 属性就是一个例子。为了将一个函数变成测试函数,需要在 `fn` 行之前加上 `#[test]`。当使用 `cargo test` 命令运行测试函数时,Rust 会构建一个测试执行者二进制文件用来运行标记了 `test` 属性的函数并报告每一个测试是通过还是失败。
|
作为最简单例子,Rust 中的测试就是一个带有 `test` 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据;第五章中结构体中用到的 `derive` 属性就是一个例子。为了将一个函数变成测试函数,需要在 `fn` 行之前加上 `#[test]`。当使用 `cargo test` 命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了 `test` 属性的函数,并报告每一个测试是通过还是失败。
|
||||||
|
|
||||||
第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然也可以额外增加任意多的测试函数以及测试模块!
|
第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然你也可以额外增加任意多的测试函数以及测试模块!
|
||||||
|
|
||||||
我们将先通过对自动生成的测试模板做一些试验来探索一些测试如何工作方面的内容,而不实际测试任何代码。接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是否正确。
|
为了厘清测试是如何工作的,我们将通过观察那些自动生成的测试模版——尽管它们实际上没有测试任何代码。接着,我们会写一些真正的测试,调用我们编写的代码并断言他们的行为的正确性。
|
||||||
|
|
||||||
让我们创建一个新的库项目 `adder`:
|
让我们创建一个新的库项目 `adder`:
|
||||||
|
|
||||||
@ -45,9 +45,9 @@ mod tests {
|
|||||||
|
|
||||||
<span class="caption">示例 11-1:由 `cargo new` 自动生成的测试模块和函数</span>
|
<span class="caption">示例 11-1:由 `cargo new` 自动生成的测试模块和函数</span>
|
||||||
|
|
||||||
现在让我们暂时忽略 `tests` 模块和 `#[cfg(test)]` 注解并只关注函数来了解其如何工作。注意 `fn` 行之前的 `#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。因为也可以在 `tests` 模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用 `#[test]` 属性标明哪些函数是测试。
|
现在让我们暂时忽略 `tests` 模块和 `#[cfg(test)]` 注解,并只关注函数来了解其如何工作。注意 `fn` 行之前的 `#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。因为也可以在 `tests` 模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用 `#[test]` 属性标明哪些函数是测试。
|
||||||
|
|
||||||
函数体使用 `assert_eq!` 宏断言 2 加 2 等于 4。这个断言作为一个典型测试格式的例子。让我们运行以便看到测试通过。
|
函数体通过使用 `assert_eq!` 宏来断言 2 加 2 等于 4。一个典型的测试的格式,就是像这个例子中的断言一样。接下来运行就可以看到测试通过。
|
||||||
|
|
||||||
`cargo test` 命令会运行项目中所有的测试,如示例 11-2 所示:
|
`cargo test` 命令会运行项目中所有的测试,如示例 11-2 所示:
|
||||||
|
|
||||||
@ -71,13 +71,13 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
|||||||
|
|
||||||
<span class="caption">示例 11-2:运行自动生成测试的输出</span>
|
<span class="caption">示例 11-2:运行自动生成测试的输出</span>
|
||||||
|
|
||||||
Cargo 编译并运行了测试。在 `Compiling`、`Finished` 和 `Running` 这几行之后,可以看到 `running 1 test` 这一行。下一行显示了生成的测试函数的名称,它是 `it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的总结:`test result: ok.` 意味着所有测试都通过了。`1 passed; 0 failed` 表示通过或失败的测试数量。
|
Cargo 编译并运行了测试。在 `Compiling`、`Finished` 和 `Running` 这几行之后,可以看到 `running 1 test` 这一行。下一行显示了生成的测试函数的名称,它是 `it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的摘要:`test result: ok.` 意味着所有测试都通过了。`1 passed; 0 failed` 表示通过或失败的测试数量。
|
||||||
|
|
||||||
这里并没有任何被标记为忽略的测试,所以总结表明 `0 ignored`。我们也没有过滤需要运行的测试,所以总结的结尾显示`0 filtered out`。在下一部分 “控制测试如何运行” 会讨论忽略和过滤测试。
|
因为之前我们并没有将任何测试标记为忽略,所以摘要中会显示 `0 ignored`。我们也没有过滤需要运行的测试,所以摘要中会显示`0 filtered out`。在下一部分 “控制测试如何运行” 会讨论忽略和过滤测试。
|
||||||
|
|
||||||
`0 measured` 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。请查看第一章来了解更多 Rust 开发版的信息。
|
`0 measured` 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。请查看第一章来了解更多 Rust 开发版的信息。
|
||||||
|
|
||||||
测试输出中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。
|
测试输出中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。
|
||||||
|
|
||||||
让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样:
|
让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样:
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ test tests::exploration ... ok
|
|||||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏。写入新测试 `another` 后, src/lib.rs` 现在看起来如示例 11-3 所示:
|
让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏。写入新测试 `another` 后, `src/lib.rs` 现在看起来如示例 11-3 所示:
|
||||||
|
|
||||||
<span class="filename">文件名: src/lib.rs</span>
|
<span class="filename">文件名: src/lib.rs</span>
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 11-3:增加第二个测试:他会因为调用了 `panic!` 宏而失败</span>
|
<span class="caption">示例 11-3:增加第二个因调用了 `panic!` 而失败的测试</span>
|
||||||
|
|
||||||
|
|
||||||
再次 `cargo test` 运行测试。输出应该看起来像示例 11-4,它表明 `exploration` 测试通过了而 `another` 失败了:
|
再次 `cargo test` 运行测试。输出应该看起来像示例 11-4,它表明 `exploration` 测试通过了而 `another` 失败了:
|
||||||
@ -147,15 +147,15 @@ error: test failed
|
|||||||
|
|
||||||
<span class="caption">示例 11-4:一个测试通过和一个测试失败的测试结果</span>
|
<span class="caption">示例 11-4:一个测试通过和一个测试失败的测试结果</span>
|
||||||
|
|
||||||
`test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为 `panicked at 'Make this test fail'` 而失败,这位于 *src/lib.rs* 的第 10 行。下一部分仅仅列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分 “控制测试如何运行” 会讲到更多运行测试的方法。
|
`test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和摘要之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为在*src/lib.rs* 的第 10 行 `panicked at 'Make this test fail'` 而失败。下一部分列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。我们可以通过使用失败测试的名称来只运行这个测试,以便调试;下一部分 “控制测试如何运行” 会讲到更多运行测试的方法。
|
||||||
|
|
||||||
最后是总结行:总体上讲,测试结果是 `FAILED`。有一个测试通过和一个测试失败。
|
最后是摘要行:总体上讲,测试结果是 `FAILED`。有一个测试通过和一个测试失败。
|
||||||
|
|
||||||
现在我们见过不同场景中测试结果是什么样子的了,再来看看除 `panic!` 之外的一些在测试中有帮助的宏吧。
|
现在我们见过不同场景中测试结果是什么样子的了,再来看看除 `panic!` 之外的一些在测试中有帮助的宏吧。
|
||||||
|
|
||||||
### 使用 `assert!` 宏来检查结果
|
### 使用 `assert!` 宏来检查结果
|
||||||
|
|
||||||
`assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个计算为布尔值的参数。如果值是 `true`,`assert!` 什么也不做同时测试会通过。如果值为 `false`,`assert!` 调用 `panic!` 宏,这会导致测试失败。`assert!` 宏帮助我们检查代码是否以期望的方式运行。
|
`assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个求值为布尔值的参数。如果值是 `true`,`assert!` 什么也不做,同时测试会通过。如果值为 `false`,`assert!` 调用 `panic!` 宏,这会导致测试失败。`assert!` 宏帮助我们检查代码是否以期望的方式运行。
|
||||||
|
|
||||||
回忆一下第五章中,示例 5-15 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用他们。将他们放进 *src/lib.rs* 并使用 `assert!` 宏编写一些测试。
|
回忆一下第五章中,示例 5-15 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用他们。将他们放进 *src/lib.rs* 并使用 `assert!` 宏编写一些测试。
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ mod tests {
|
|||||||
|
|
||||||
<span class="caption">示例 11-6:一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形</span>
|
<span class="caption">示例 11-6:一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形</span>
|
||||||
|
|
||||||
注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 “私有性规则” 部分介绍的常用可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在 `tests` 模块中都是可用的。
|
注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 “私有性规则” 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。
|
||||||
|
|
||||||
我们将测试命名为 `larger_can_hold_smaller`,并创建所需的两个 `Rectangle` 实例。接着调用 `assert!` 宏并传递 `larger.can_hold(&smaller)` 调用的结果作为参数。这个表达式预期会返回 `true`,所以测试应该通过。让我们拭目以待!
|
我们将测试命名为 `larger_can_hold_smaller`,并创建所需的两个 `Rectangle` 实例。接着调用 `assert!` 宏并传递 `larger.can_hold(&smaller)` 调用的结果作为参数。这个表达式预期会返回 `true`,所以测试应该通过。让我们拭目以待!
|
||||||
|
|
||||||
@ -233,7 +233,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
因为这里 `can_hold` 函数的正确结果是 `false`,我们需要将这个结果取反后传递给 `assert!` 宏。这样的话,测试就会通过而 `can_hold` 将返回`false`:
|
因为这里 `can_hold` 函数的正确结果是 `false` ,我们需要将这个结果取反后传递给 `assert!` 宏。因此 `can_hold` 返回 `false` 时测试就会通过:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
running 2 tests
|
running 2 tests
|
||||||
@ -284,7 +284,7 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
|||||||
|
|
||||||
### 使用 `assert_eq!` 和 `assert_ne!` 宏来测试相等
|
### 使用 `assert_eq!` 和 `assert_ne!` 宏来测试相等
|
||||||
|
|
||||||
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `assert!` 宏传递一个使用 `==` 运算符的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来更方便的处理这些操作:`assert_eq!` 和 `assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 **为什么** 失败,而 `assert!` 只会打印出它从 `==` 表达式中得到了 `false` 值,而不是导致 `false` 的两个值。
|
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `assert!` 宏传递一个使用 `==` 运算符的表达式来做到。不过这个操作实在是太常见了,以至于标准库提供了一对宏来更方便的处理这些操作:`assert_eq!` 和 `assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 **为什么** 失败,而 `assert!` 只会打印出它从 `==` 表达式中得到了 `false` 值,而不是导致 `false` 的两个值。
|
||||||
|
|
||||||
示例 11-7 中,让我们编写一个对其参数加二并返回结果的函数 `add_two`。接着使用 `assert_eq!` 宏测试这个函数:
|
示例 11-7 中,让我们编写一个对其参数加二并返回结果的函数 `add_two`。接着使用 `assert_eq!` 宏测试这个函数:
|
||||||
|
|
||||||
@ -317,7 +317,7 @@ test tests::it_adds_two ... ok
|
|||||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
传递给 `assert_eq!` 宏的第一个参数,4,等于调用 `add_two(2)` 的结果。我们将会看到这个测试的那一行说 `test tests::it_adds_two ... ok`,`ok` 表明测试通过了!
|
传递给 `assert_eq!` 宏的第一个参数 `4` ,等于调用 `add_two(2)` 的结果。测试中的这一行 `test tests::it_adds_two ... ok` 中 `ok` 表明测试通过!
|
||||||
|
|
||||||
在代码中引入一个 bug 来看看使用 `assert_eq!` 的测试失败是什么样的。修改 `add_two` 函数的实现使其加 3:
|
在代码中引入一个 bug 来看看使用 `assert_eq!` 的测试失败是什么样的。修改 `add_two` 函数的实现使其加 3:
|
||||||
|
|
||||||
@ -349,15 +349,15 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
|||||||
|
|
||||||
测试捕获到了 bug!`it_adds_two` 测试失败,显示信息 `` assertion failed: `(left == right)` `` 并表明 `left` 是 `4` 而 `right` 是 `5`。这个信息有助于我们开始调试:它说 `assert_eq!` 的 `left` 参数是 `4`,而 `right` 参数,也就是 `add_two(2)` 的结果,是 `5`。
|
测试捕获到了 bug!`it_adds_two` 测试失败,显示信息 `` assertion failed: `(left == right)` `` 并表明 `left` 是 `4` 而 `right` 是 `5`。这个信息有助于我们开始调试:它说 `assert_eq!` 的 `left` 参数是 `4`,而 `right` 参数,也就是 `add_two(2)` 的结果,是 `5`。
|
||||||
|
|
||||||
注意在一些语言和测试框架中,断言两个值相等的函数的参数叫做 `expected` 和 `actual`,而且指定参数的顺序是需要注意的。然而在 Rust 中,他们则叫做 `left` 和 `right`,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成 `assert_eq!(add_two(2), 4)`,这时错误信息会变成 `` assertion failed: `(left == right)` `` 其中 `left` 是 `5` 而 `right` 是 `4`。
|
需要注意的是,在一些语言和测试框架中,断言两个值相等的函数的参数叫做 `expected` 和 `actual`,而且指定参数的顺序是很关键的。然而在 Rust 中,他们则叫做 `left` 和 `right`,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成 `assert_eq!(add_two(2), 4)`,这时失败信息会变成 `` assertion failed: `(left == right)` `` 其中 `left` 是 `5` 而 `right` 是 `4`。
|
||||||
|
|
||||||
`assert_ne!` 宏在传递给它的两个值不相等时通过而在相等时失败。这个宏在代码按照我们期望运行时不确定值 **会** 是什么,不过知道他们绝对 **不会** 是什么的时候最有用处。例如,如果一个函数确定会以某种方式改变其输出,不过这种方式由运行测试是星期几来决定,这时最好的断言可能就是函数的输出不等于其输入。
|
`assert_ne!` 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 **会** 是什么,不过能确定值绝对 **不会** 是什么的时候,这个宏最有用处。例如,如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。
|
||||||
|
|
||||||
`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C 中有更多关于这些和其他派生 trait 的详细信息。
|
`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C 中有更多关于这些和其他派生 trait 的详细信息。
|
||||||
|
|
||||||
### 自定义错误信息
|
### 自定义失败信息
|
||||||
|
|
||||||
也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的参数来增加用于打印的自定义错误信息。任何在 `assert!` 必需的一个参数和 `assert_eq!` 和 `assert_ne!` 必需的两个参数之后指定的参数都会传递给第八章讲到的 `format!` 宏,所以可以传递一个包含 `{}` 占位符的格式字符串和放入占位符的值。自定义信息有助于记录断言的意义,这样到测试失败时,就能更好的理解代码出了什么问题。
|
你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!` 和 `assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第八章的“使用 `+` 运算符或 `format!` 宏连接字符串”部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。
|
||||||
|
|
||||||
例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
|
例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
|
||||||
|
|
||||||
@ -380,9 +380,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这个程序的需求还没有被确定,而我们非常确定问候开始的 `Hello` 文本不会改变。我们决定并不想在人名改变时不得不更新测试,所以相比检查 `greeting` 函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。
|
这个程序的需求还没有被确定,因此问候文本开头的 `Hello` 文本很可能会改变。然而我们并不想在需求改变时不得不更新测试,所以相比检查 `greeting` 函数返回的确切值,我们将仅仅断言输出的文本中包含输入参数。
|
||||||
|
|
||||||
让我们通过将 `greeting` 改为不包含 `name` 来在代码中引入一个 bug 来测试失败时是怎样的,
|
让我们通过将 `greeting` 改为不包含 `name` 来在代码中引入一个 bug 来测试失败时是怎样的:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub fn greeting(name: &str) -> String {
|
pub fn greeting(name: &str) -> String {
|
||||||
@ -407,7 +407,7 @@ failures:
|
|||||||
tests::greeting_contains_name
|
tests::greeting_contains_name
|
||||||
```
|
```
|
||||||
|
|
||||||
这仅仅告诉了我们断言失败了和失败的行号。一个更有用的错误信息应该打印出从 `greeting` 函数得到的值。让我们改变测试函数来使用一个由包含占位符的格式字符串和从 `greeting` 函数取得的值组成的自定义错误信息:
|
这仅仅告诉了我们断言失败了和失败的行号。一个更有用的失败信息应该打印出 `greeting` 函数的值。让我们为测试函数增加一个自定义失败信息参数:带占位符的格式字符串,以及 `greeting` 函数的值:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
#[test]
|
#[test]
|
||||||
@ -420,7 +420,7 @@ fn greeting_contains_name() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
现在如果再次运行测试,将会看到更有价值的错误信息:
|
现在如果再次运行测试,将会看到更有价值的信息:
|
||||||
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@ -430,15 +430,15 @@ contain name, value was `Hello!`', src/lib.rs:12:8
|
|||||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||||
```
|
```
|
||||||
|
|
||||||
可以在测试输出中看到所取得的确切的值,这会帮助我们理解真正发生了什么而不是期望发生什么。
|
可以在测试输出中看到所取得的确切的值,这会帮助我们理解真正发生了什么,而不是期望发生什么。
|
||||||
|
|
||||||
### 使用 `should_panic` 检查 panic
|
### 使用 `should_panic` 检查 panic
|
||||||
|
|
||||||
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章示例 9-9 创建的 `Guess` 类型。其他使用 `Guess` 的代码依赖于 `Guess` 实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。
|
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误也是很重要的。例如,考虑第九章示例 9-9 创建的 `Guess` 类型。其他使用 `Guess` 的代码都是基于 `Guess` 实例仅有的值范围在 1 到 100 的前提。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。
|
||||||
|
|
||||||
可以通过对函数增加另一个属性 `should_panic` 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。
|
可以通过对函数增加另一个属性 `should_panic` 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。
|
||||||
|
|
||||||
示例 11-8 展示了如何编写一个测试来检查 `Guess::new` 按照我们的期望出现的错误情况:
|
示例 11-8 展示了一个检查 `Guess::new` 是否按照我们的期望出错的测试:
|
||||||
|
|
||||||
<span class="filename">文件名: src/lib.rs</span>
|
<span class="filename">文件名: src/lib.rs</span>
|
||||||
|
|
||||||
@ -473,7 +473,7 @@ mod tests {
|
|||||||
|
|
||||||
<span class="caption">示例 11-8:测试会造成 `panic!` 的条件</span>
|
<span class="caption">示例 11-8:测试会造成 `panic!` 的条件</span>
|
||||||
|
|
||||||
`#[should_panic]` 属性位于 `#[test]` 之后和对应的测试函数之前。让我们看看测试通过时它是什么样子:
|
`#[should_panic]` 属性位于 `#[test]` 之后,对应的测试函数之前。让我们看看测试通过时它是什么样子:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
running 1 test
|
running 1 test
|
||||||
@ -520,7 +520,7 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
|||||||
|
|
||||||
这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了 `#[should_panic]`。这个错误意味着代码中函数 `Guess::new(200)` 并没有产生 panic。
|
这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了 `#[should_panic]`。这个错误意味着代码中函数 `Guess::new(200)` 并没有产生 panic。
|
||||||
|
|
||||||
然而 `should_panic` 测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。`should_panic` 甚至在测试因为其他不同的原因而不是我们期望发生的情况而 panic 时也会通过。为了使 `should_panic` 测试更精确,可以给 `should_panic` 属性增加一个可选的 `expected` 参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑示例 11-9 中修改过的 `Guess`,这里 `new` 函数根据其值是过大还或者过小而提供不同的 panic 信息:
|
然而 `should_panic` 测试结果可能会非常含糊不清,因为它只是告诉我们代码并没有产生 panic。`should_panic` 甚至在一些不是我们期望的原因而导致 panic 时也会通过。为了使 `should_panic` 测试结果更精确,我们可以给 `should_panic` 属性增加一个可选的 `expected` 参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑示例 11-9 中修改过的 `Guess`,这里 `new` 函数根据其值是过大还或者过小而提供不同的 panic 信息:
|
||||||
|
|
||||||
<span class="filename">文件名: src/lib.rs</span>
|
<span class="filename">文件名: src/lib.rs</span>
|
||||||
|
|
||||||
@ -561,7 +561,7 @@ mod tests {
|
|||||||
|
|
||||||
<span class="caption">示例 11-9:一个会带有特定错误信息的 `panic!` 条件的测试</span>
|
<span class="caption">示例 11-9:一个会带有特定错误信息的 `panic!` 条件的测试</span>
|
||||||
|
|
||||||
这个测试会通过,因为 `should_panic` 属性中 `expected` 参数提供的值是 `Guess::new` 函数 panic 信息的子字符串。我们可以指定期望的整个 panic 信息,在这个例子中是 `Guess value must be less than or equal to 100, got 200.`。这依赖于 panic 有多独特或动态,和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在 `else if value > 100` 的情况下运行。
|
这个测试会通过,因为 `should_panic` 属性中 `expected` 参数提供的值是 `Guess::new` 函数 panic 信息的子串。我们可以指定期望的整个 panic 信息,在这个例子中是 `Guess value must be less than or equal to 100, got 200.` 。 `expected` 信息的选择取决于 panic 信息有多独特或动态,和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在 `else if value > 100` 的情况下运行。
|
||||||
|
|
||||||
为了观察带有 `expected` 信息的 `should_panic` 测试失败时会发生什么,让我们再次引入一个 bug,将 `if value < 1` 和 `else if value > 100` 的代码块对换:
|
为了观察带有 `expected` 信息的 `should_panic` 测试失败时会发生什么,让我们再次引入一个 bug,将 `if value < 1` 和 `else if value > 100` 的代码块对换:
|
||||||
|
|
||||||
@ -594,6 +594,6 @@ failures:
|
|||||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
错误信息表明测试确实如期望 panic 了,不过 panic 信息是 `did not include expected string 'Guess value must be less than or equal to 100'`。可以看到我们得到的 panic 信息,在这个例子中是 `Guess value must be greater than or equal to 1, got 200.`。这样就可以开始寻找 bug 在哪了!
|
失败信息表明测试确实如期望 panic 了,不过 panic 信息中并没有包含 `expected` 信息 `'Guess value must be less than or equal to 100'`。而我们得到的 panic 信息是 `'Guess value must be greater than or equal to 1, got 200.'`。这样就可以开始寻找 bug 在哪了!
|
||||||
|
|
||||||
现在你知道了几种编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于 `cargo test` 的不同选项。
|
现在你知道了几种编写测试的方法,让我们看看运行测试时会发生什么,和可以用于 `cargo test` 的不同选项。
|
||||||
|
@ -4,29 +4,29 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 550c8ea6f74060ff1f7b67e7e1878c4da121682d
|
> commit 550c8ea6f74060ff1f7b67e7e1878c4da121682d
|
||||||
|
|
||||||
就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。可以指定命令行参数来改变 `cargo test` 的默认行为。例如,`cargo test` 生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过程中产生的输出避免他们被显示出来,使得阅读测试结果相关的内容变得更容易。
|
就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。可以指定命令行参数来改变 `cargo test` 的默认行为。例如,`cargo test` 生成的二进制文件的默认行为是并行的运行所有测试,并截获测试运行过程中产生的输出,阻止他们被显示出来,使得阅读测试结果相关的内容变得更容易。
|
||||||
|
|
||||||
这些选项的一部分可以传递给 `cargo test`,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给 `cargo test` 的参数,接着是分隔符 `--`,再之后是传递给测试二进制文件的参数。运行 `cargo test --help` 会告诉你 `cargo test` 的相关参数,而运行 `cargo test -- --help` 则会告诉你位于分隔符 `--` 之后的相关参数。
|
这些选项的一部分可以传递给 `cargo test`,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给 `cargo test` 的参数,接着是分隔符 `--`,再之后是传递给测试二进制文件的参数。运行 `cargo test --help` 会告诉你 `cargo test` 的相关参数,而运行 `cargo test -- --help` 则会告诉你可以在分隔符 `--` 之后使用的相关参数。
|
||||||
|
|
||||||
### 并行或连续的运行测试
|
### 并行或连续的运行测试
|
||||||
|
|
||||||
当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或依赖任何共享状态,这包括类似于当前工作目录或者环境变量这样的共享环境。
|
当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。
|
||||||
|
|
||||||
例如,每一个测试都运行一些代码在硬盘上创建一个 `test-output.txt` 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。
|
举个例子,每一个测试都运行一些代码,假设这些代码都在硬盘上创建一个 *test-output.txt* 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中修改了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干扰。一个解决方案是使每一个测试读写不同的文件;另一个解决方案是一次运行一个测试。
|
||||||
|
|
||||||
如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递 `--test-threads` 参数和希望使用线程的数量给测试二进制文件。例如:
|
如果你不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递 `--test-threads` 参数和希望使用线程的数量给测试二进制文件。例如:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ cargo test -- --test-threads=1
|
$ cargo test -- --test-threads=1
|
||||||
```
|
```
|
||||||
|
|
||||||
这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过测试就不会在存在共享状态时潜在的相互干涉了。
|
这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。
|
||||||
|
|
||||||
### 显示函数输出
|
### 显示函数输出
|
||||||
|
|
||||||
如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用 `println!` 而测试通过了,我们将不会在终端看到 `println!` 的输出:只会看到说明测试通过的行。如果测试失败了,就会看到所有标准输出和其他错误信息。
|
默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容。比如在测试中调用了 `println!` 而测试通过了,我们将不会在终端看到 `println!` 的输出:只会看到说明测试通过的提示行。如果测试失败了,则会看到所有标准输出和其他错误信息。
|
||||||
|
|
||||||
例如,示例 11-10 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:
|
例如,示例 11-10 有一个无意义的函数,它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:
|
||||||
|
|
||||||
<span class="filename">文件名: src/lib.rs</span>
|
<span class="filename">文件名: src/lib.rs</span>
|
||||||
|
|
||||||
@ -78,9 +78,9 @@ failures:
|
|||||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
注意输出中哪里也不会出现 `I got the value 4`,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,`I got the value 8`,则出现在输出的测试总结部分,同时也显示了测试失败的原因。
|
注意输出中不会出现测试通过时打印的内容,即 `I got the value 4`。因为当测试通过时,这些输出会被截获。失败测试的输出 `I got the value 8` ,则出现在输出的测试摘要部分,同时也显示了测试失败的原因。
|
||||||
|
|
||||||
如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过 `--nocapture` 参数来禁用:
|
如果你希望也能看到通过的测试中打印的值,截获输出的行为可以通过 `--nocapture` 参数来禁用:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ cargo test -- --nocapture
|
$ cargo test -- --nocapture
|
||||||
@ -107,13 +107,13 @@ failures:
|
|||||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用 `--test-threads=1` 和 `--nocapture` 功能来看看输出是什么样子!
|
注意测试的输出和测试结果的输出是相互交叉的;这是由于测试是并行运行的,也就是上一部分讲到的。尝试一同使用 `--test-threads=1` 和 `--nocapture` 功能来看看输出是什么样子!
|
||||||
|
|
||||||
### 通过名称来运行测试的子集
|
### 通过名称来运行测试的子集
|
||||||
|
|
||||||
有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这些代码相关的测试。可以向 `cargo test` 传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。
|
有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这些代码相关的测试。你可以向 `cargo test` 传递希望运行的测试的部分名称作为参数来选择运行哪些测试。
|
||||||
|
|
||||||
为了展示如何运行测试的子集,示例 11-11 为 `add_two` 函数创建了三个测试来供我们选择运行哪一个:
|
为了展示如何运行测试的子集,示例 11-11 为 `add_two` 函数创建了三个测试,我们可以选择具体运行哪一个:
|
||||||
|
|
||||||
<span class="filename">文件名: src/lib.rs</span>
|
<span class="filename">文件名: src/lib.rs</span>
|
||||||
|
|
||||||
@ -171,13 +171,13 @@ test tests::one_hundred ... ok
|
|||||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
|
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
只有名称为 `one_hundred` 的测试被运行了;其余两个测试并不匹配这个名称。测试输出在总结行的结尾显示了 `2 filtered out` 表明存在比本命令所运行的更多的测试。
|
只有名称为 `one_hundred` 的测试被运行了;因为其余两个测试并不匹配这个名称。测试输出在摘要行的结尾显示了 `2 filtered out` 表明还存在比本次所运行的测试更多的测试被过滤掉了。
|
||||||
|
|
||||||
不能像这样指定多个测试名称,只有传递给 `cargo test` 的第一个值才会被使用。不过有运行多个测试的方法。
|
不能像这样指定多个测试名称;只有传递给 `cargo test` 的第一个值才会被使用。不过有运行多个测试的方法。
|
||||||
|
|
||||||
#### 过滤运行多个测试
|
#### 过滤运行多个测试
|
||||||
|
|
||||||
然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含 `add`,可以通过 `cargo test add` 来运行这两个测试:
|
我们可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行。例如,因为头两个测试的名称包含 `add`,可以通过 `cargo test add` 来运行这两个测试:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ cargo test add
|
$ cargo test add
|
||||||
@ -191,11 +191,11 @@ test tests::add_three_and_two ... ok
|
|||||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
|
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
这运行了所有名字中带有 `add` 的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。
|
这运行了所有名字中带有 `add` 的测试,也过滤掉了名为 `one_hundred` 的测试。同时注意测试所在的模块也是测试名称的一部分,所以可以通过模块名来运行一个模块中的所有测试。
|
||||||
|
|
||||||
### 除非指定否则忽略某些测试
|
### 忽略某些测试
|
||||||
|
|
||||||
有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 `cargo test` 的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用 `ignore` 属性来标记耗时的测试并排除他们,如下所示:
|
有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 `cargo test` 的时候希望能排除他们。虽然可以通过参数列举出所有希望运行的测试来做到,也可以使用 `ignore` 属性来标记耗时的测试并排除他们,如下所示:
|
||||||
|
|
||||||
<span class="filename">文件名: src/lib.rs</span>
|
<span class="filename">文件名: src/lib.rs</span>
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ fn expensive_test() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
对想要排除的测试的 `#[test]` 之后增加了 `#[ignore]` 行。现在如果运行测试,就会发现 `it_works` 运行了,而 `expensive_test` 没有运行:
|
对于想要排除的测试,我们在 `#[test]` 之后增加了 `#[ignore]` 行。现在如果运行测试,就会发现 `it_works` 运行了,而 `expensive_test` 没有运行:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ cargo test
|
$ cargo test
|
||||||
@ -227,7 +227,7 @@ test it_works ... ok
|
|||||||
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
|
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
`expensive_test` 被列为 `ignored`,如果只希望运行被忽略的测试,可以使用 `cargo test -- --ignored`:
|
`expensive_test` 被列为 `ignored`,如果我们只希望运行被忽略的测试,可以使用 `cargo test -- --ignored`:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ cargo test -- --ignored
|
$ cargo test -- --ignored
|
||||||
@ -240,4 +240,4 @@ test expensive_test ... ok
|
|||||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
|
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
通过控制运行哪些测试,可以确保运行 `cargo test` 的结果是快速的。当某个时刻需要检查 `ignored` 测试的结果而且你也有时间等待这个结果的话,可以选择执行 `cargo test -- --ignored`。
|
通过控制运行哪些测试,你可以确保能够快速地运行 `cargo test` 。当某个时刻需要检查 `ignored` 测试的结果,而且你也有时间等待这个结果的话,就可以选择执行 `cargo test -- --ignored`。
|
||||||
|
@ -4,17 +4,17 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit b3eddb8edc0c3f83647143673d18efac0a44083a
|
> commit b3eddb8edc0c3f83647143673d18efac0a44083a
|
||||||
|
|
||||||
正如之前提到的,测试是一个复杂的概念,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与 **集成测试**(*integration tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块,也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户采用相同的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。
|
本章一开始就提到,测试是一个复杂的概念,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与 **集成测试**(*integration tests*)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。
|
||||||
|
|
||||||
编写这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
|
为了保证你的库能够按照你的预期运行,从独立和整体的角度编写这两类测试都是非常重要的。
|
||||||
|
|
||||||
### 单元测试
|
### 单元测试
|
||||||
|
|
||||||
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位代码位于何处和是否符合预期。单元测试位于 *src* 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的 `tests` 模块,并使用 `cfg(test)` 标注模块。
|
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的某个单元的代码功能是否符合预期。单元测试与他们要测试的代码共同存放在位于 *src* 目录下相同的文件中。规范是在每个文件中创建包含测试函数的 `tests` 模块,并使用 `cfg(test)` 标注模块。
|
||||||
|
|
||||||
#### 测试模块和 `cfg(test)`
|
#### 测试模块和 `cfg(test)`
|
||||||
|
|
||||||
测试模块的 `#[cfg(test)]` 注解告诉 Rust 只在执行 `cargo test` 时才编译和运行测试代码,而在运行 `cargo build` 时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹,他们并不需要 `#[cfg(test)]` 注解。但是因为单元测试位于与源码相同的文件中,所以使用 `#[cfg(test)]` 来指定他们不应该被包含进编译结果中。
|
测试模块的 `#[cfg(test)]` 注解告诉 Rust 只在执行 `cargo test` 时才编译和运行测试代码,而在运行 `cargo build` 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 `#[cfg(test)]` 注解。然而单元测试位于与源码相同的文件中,所以你需要使用 `#[cfg(test)]` 来指定他们不应该被包含进编译结果中。
|
||||||
|
|
||||||
还记得本章第一部分新建的 `adder` 项目吗?Cargo 为我们生成了如下代码:
|
还记得本章第一部分新建的 `adder` 项目吗?Cargo 为我们生成了如下代码:
|
||||||
|
|
||||||
@ -30,11 +30,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这里自动生成了测试模块。`cfg` 属性代表 *configuration* ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是 `test`,Rust 所提供的用于编译和运行测试的配置。通过使用这个属性,Cargo 只会在我们主动使用 `cargo test` 运行测试时才编译测试代码。除了标注为 `#[test]` 的函数之外,还包括测试模块中可能存在的帮助函数。
|
上述代码就是自动生成的测试模块。`cfg` 属性代表 *configuration* ,它告诉 Rust 其之后的项只应该被包含进特定配置选项中。在这个例子中,配置选项是 `test`,即 Rust 所提供的用于编译和运行测试的配置选项。通过使用 `cfg` 属性,Cargo 只会在我们主动使用 `cargo test` 运行测试时才编译测试代码。需要编译的不仅仅有标注为 `#[test]` 的函数之外,还包括测试模块中可能存在的帮助函数。
|
||||||
|
|
||||||
#### 测试私有函数
|
#### 测试私有函数
|
||||||
|
|
||||||
测试社区中一直存在关于是否应该对私有函数进行单元测试的论战,而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑示例 11-12 中带有私有函数 `internal_adder` 的代码:
|
测试社区中一直存在关于是否应该对私有函数直接进行测试的论战,而在其他语言中想要测试私有函数是一件困难的,甚至是不可能的事。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数。考虑示例 11-12 中带有私有函数 `internal_adder` 的代码:
|
||||||
|
|
||||||
<span class="filename">文件名: src/lib.rs</span>
|
<span class="filename">文件名: src/lib.rs</span>
|
||||||
|
|
||||||
@ -60,17 +60,17 @@ mod tests {
|
|||||||
|
|
||||||
<span class="caption">示例 11-12:测试私有函数</span>
|
<span class="caption">示例 11-12:测试私有函数</span>
|
||||||
|
|
||||||
注意 `internal_adder` 函数并没有标记为 `pub`,不过因为测试也不过是 Rust 代码同时 `tests` 也仅仅是另一个模块,我们完全可以在测试中导入和调用 `internal_adder`。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。
|
注意 `internal_adder` 函数并没有标记为 `pub`,不过因为测试也不过是 Rust 代码同时 `tests` 也仅仅是另一个模块,我们完全可以在测试中导入和调用 `internal_adder`。如果你并不认为应该测试私有函数,Rust 也不会强迫你这么做。
|
||||||
|
|
||||||
### 集成测试
|
### 集成测试
|
||||||
|
|
||||||
在 Rust 中,集成测试对于需要测试的库来完全说是外部的。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 *tests* 目录。
|
在 Rust 中,集成测试对于你需要测试的库来说完全是外部的。同其他使用库的代码一样使用库文件,也就是说它们只能调用一部分库中的公有 API 。集成测试的目的是测试库的多个部分能否一起正常工作。一些单独能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,你需要先创建一个 *tests* 目录。
|
||||||
|
|
||||||
#### *tests* 目录
|
#### *tests* 目录
|
||||||
|
|
||||||
为了编写集成测试,需要在项目根目录创建一个 *tests* 目录,与 *src* 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。
|
为了编写集成测试,需要在项目根目录创建一个 *tests* 目录,与 *src* 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。
|
||||||
|
|
||||||
让我们来创建一个集成测试!保留示例 11-12 中 *src/lib.rs* 的代码。创建一个 *tests* 目录,新建一个文件 *tests/integration_test.rs*,并输入示例 11-13 中的代码。
|
让我们来创建一个集成测试。保留示例 11-12 中 *src/lib.rs* 的代码。创建一个 *tests* 目录,新建一个文件 *tests/integration_test.rs*,并输入示例 11-13 中的代码。
|
||||||
|
|
||||||
<span class="filename">文件名: tests/integration_test.rs</span>
|
<span class="filename">文件名: tests/integration_test.rs</span>
|
||||||
|
|
||||||
@ -114,13 +114,13 @@ running 0 tests
|
|||||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(示例 11-12 中有一个叫做 `internal` 的测试),接着是一个单元测试的总结行。
|
现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每个单元测试一行(示例 11-12 中有一个叫做 `internal` 的测试),接着是一个单元测试的摘要行。
|
||||||
|
|
||||||
集成测试部分以行 `Running target/debug/deps/integration-test-ce99bcc2479f4607`(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在 `Doc-tests adder` 部分开始之前的集成测试的总结行。
|
集成测试部分以行 `Running target/debug/deps/integration-test-ce99bcc2479f4607`(在输出最后的哈希值可能不同)开头。接下来每一行是一个集成测试中的测试函数,以及一个位于 `Doc-tests adder` 部分之前的集成测试的摘要行。
|
||||||
|
|
||||||
注意在任意 *src* 文件中增加更多单元测试函数会增加更多单元测试部分的测试结果行。在我们创建的集成测试文件中增加更多测试函数会增加更多集成测试部分的行。每一个集成测试文件有其自己的部分,所以如果在 *tests* 目录中增加更多文件,这里就会有更多集成测试部分。
|
我们已经知道,单元测试函数越多,单元测试部分的结果行就会越多。同样的,在集成文件中增加的测试函数越多,也会在对应的测试结果部分增加越多的结果行。每一个集成测试文件有对应的测试结果部分,所以如果在 *tests* 目录中增加更多文件,测试结果中就会有更多集成测试结果部分。
|
||||||
|
|
||||||
我们仍然可以通过指定测试函数的名称作为 `cargo test` 的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用 `cargo test` 的 `--test` 后跟文件的名称:
|
我们仍然可以通过指定测试函数的名称作为 `cargo test` 的参数来运行特定集成测试。也可以使用 `cargo test` 的 `--test` 后跟文件的名称来运行某个特定集成测试文件中的所有测试:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ cargo test --test integration_test
|
$ cargo test --test integration_test
|
||||||
@ -133,15 +133,15 @@ test it_adds_two ... ok
|
|||||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
这些只是 *tests* 目录中我们指定的文件中的测试。
|
这个命令只运行了 *tests* 目录中我们指定的文件 `integration_test.rs` 中的测试。
|
||||||
|
|
||||||
#### 集成测试中的子模块
|
#### 集成测试中的子模块
|
||||||
|
|
||||||
随着集成测试的增加,你可能希望在 `tests` 目录增加更多文件辅助组织他们,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 *tests* 目录中的文件都被编译为单独的 crate。
|
随着集成测试的增加,你可能希望在 `tests` 目录增加更多文件以便更好的组织他们,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 *tests* 目录中的文件都被编译为单独的 crate。
|
||||||
|
|
||||||
将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到第七章学习的如何将代码分隔进模块和文件的知识,*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为。
|
将每个集成测试文件当作其自己的 crate 来对待,这更有助于创建单独的作用域,这种单独的作用域能提供更类似与最终使用者使用 crate 的环境。然而,正如你在第七章中学习的如何将代码分为模块和文件的知识,*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为。
|
||||||
|
|
||||||
对于 *tests* 目录中不同文件的行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章 “将模块移动到其他文件” 部分的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 *tests/common.rs* 并将 `setup` 函数放入其中,这里将放入一些我们希望能够在多个测试文件的多个测试函数中调用的代码:
|
当你有一些在多个集成测试文件都会用到的帮助函数,而你尝试按照第七章 “将模块移动到其他文件” 部分的步骤将他们提取到一个通用的模块中时, *tests* 目录中不同文件的行为就会显得很明显。例如,如果我们创建了 *tests/common.rs* 文件并将一个名叫 `setup` 的函数放入其中,这里将放入一些我们希望能够在多个测试文件的多个测试函数中调用的代码:
|
||||||
|
|
||||||
<span class="filename">文件名: tests/common.rs</span>
|
<span class="filename">文件名: tests/common.rs</span>
|
||||||
|
|
||||||
@ -151,7 +151,7 @@ pub fn setup() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
如果再次运行测试,将会在测试结果中看到一个对应 *common.rs* 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了 `setup` 函数:
|
如果再次运行测试,将会在测试结果中看到一个新的对应 *common.rs* 文件的测试结果部分,即便这个文件并没有包含任何测试函数,也没有任何地方调用了 `setup` 函数:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
running 1 test
|
running 1 test
|
||||||
@ -179,11 +179,11 @@ running 0 tests
|
|||||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
```
|
```
|
||||||
|
|
||||||
`common` 出现在测试结果中并显示 `running 0 tests`,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。
|
我们并不希望 `common` 出现在测试结果中并显示 `running 0 tests` 。我们只是希望能够在其他集成测试文件中分享一些代码罢了。
|
||||||
|
|
||||||
为了避免 `common` 出现在测试输出中,不同于创建 *tests/common.rs*,我们将创建 *tests/common/mod.rs*。在第七章的 “模块文件系统规则” 部分,对于拥有子模块的模块文件使用了 *module_name/mod.rs* 命名规范,虽然这里 `common` 并没有子模块,但是这样命名告诉 Rust 不要将 `common` 看作一个集成测试文件。当将 `setup` 代码移动到 *tests/common/mod.rs* 并去掉 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。
|
为了避免 `common` 出现在测试输出中,我们将创建 *tests/common/mod.rs* ,而不是创建 *tests/common.rs* 。在第七章的 “模块文件系统规则” 部分,对于拥有子模块的模块文件使用了 *module_name/mod.rs* 命名规范,虽然这里 `common` 并没有子模块,但是这样命名告诉 Rust 不要将 `common` 看作一个集成测试文件。当将 `setup` 函数代码移动到 *tests/common/mod.rs* 并删除 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中。
|
||||||
|
|
||||||
一旦拥有了 *tests/common/mod.rs*,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 *tests/integration_test.rs* 中调用 `setup` 函数的 `it_adds_two` 测试的例子:
|
一旦拥有了 *tests/common/mod.rs*,就可以将其作为模块以便在任何集成测试文件中使用。这里是一个 *tests/integration_test.rs* 中调用 `setup` 函数的 `it_adds_two` 测试的例子:
|
||||||
|
|
||||||
<span class="filename">文件名: tests/integration_test.rs</span>
|
<span class="filename">文件名: tests/integration_test.rs</span>
|
||||||
|
|
||||||
@ -203,12 +203,12 @@ fn it_adds_two() {
|
|||||||
|
|
||||||
#### 二进制 crate 的集成测试
|
#### 二进制 crate 的集成测试
|
||||||
|
|
||||||
如果项目是二进制 crate 并且只包含 *src/main.rs* 而没有 *src/lib.rs*,这样就不可能在 *tests* 创建集成测试并使用 `extern crate` 导入 *src/main.rs* 中的函数了。只有库 crate 向其他 crate 暴露了可供调用和使用的函数;二进制 crate 只意在单独运行。
|
如果项目是二进制 crate 并且只包含 *src/main.rs* 而没有 *src/lib.rs*,这样就不可能在 *tests* 目录创建集成测试并使用 `extern crate` 导入 *src/main.rs* 中定义的函数。只有库 crate 才会向其他 crate 暴露了可供调用和使用的函数;二进制 crate 只意在单独运行。
|
||||||
|
|
||||||
这也是 Rust 二进制项目明确采用 *src/main.rs* 调用 *src/lib.rs* 中逻辑这样的结构的原因之一。通过这种结构,集成测试 **就可以** 使用 `extern crate` 测试库 crate 中的主要功能了,而如果这些重要的功能没有问题的话,*src/main.rs* 中的少量代码也就会正常工作且不需要测试。
|
为什么 Rust 二进制项目的结构明确采用 *src/main.rs* 调用 *src/lib.rs* 中的逻辑的方式?因为通过这种结构,集成测试 **就可以** 通过 `extern crate` 测试库 crate 中的主要功能了,而如果这些重要的功能没有问题的话,*src/main.rs* 中的少量代码也就会正常工作且不需要测试。
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
Rust 的测试功能提供了一个如何确保即使函数做出改变也能继续以期望的方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他外部代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 仍然是很重要的。
|
Rust 的测试功能提供了一个确保即使你改变了函数的实现方式,也能继续以期望的方式运行的途径。单元测试独立地验证库的不同部分,也能够测试私有函数实现细节。集成测试则检查多个部分是否能结合起来正确地工作,并像其他外部代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码中不符合期望行为的逻辑 bug 仍然是很重要的。
|
||||||
|
|
||||||
接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!
|
让我们将本章和其他之前章节所学的知识组合起来,在下一章一起编写一个项目!
|
||||||
|
Loading…
Reference in New Issue
Block a user