mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 00:43:59 +08:00
check to ch12-02
This commit is contained in:
parent
5f3aa59bb8
commit
bb2f12c6c7
@ -29,7 +29,7 @@
|
||||
|
||||
<span class="caption">示例 10-18:尝试使用离开作用域的值的引用</span>
|
||||
|
||||
> ### 未初始化变量不能被使用
|
||||
> #### 未初始化变量不能被使用
|
||||
>
|
||||
> 接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!
|
||||
|
||||
@ -47,7 +47,7 @@ error: `x` does not live long enough
|
||||
| - borrowed value needs to live until here
|
||||
```
|
||||
|
||||
变量 `x` 并没有 “存在的足够久”。为什么呢?好吧,`x` 在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过 `r` 在外部作用域也是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r`将会引用在`x`离开作用域时被释放的内存,这时尝试对`r`做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?
|
||||
变量 `x` 并没有 “存在的足够久”。为什么呢?好吧,`x` 在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过 `r` 在外部作用域也是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r` 将会引用在 `x` 离开作用域时被释放的内存,这时尝试对 `r` 做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?
|
||||
|
||||
#### 借用检查器
|
||||
|
||||
@ -55,17 +55,15 @@ error: `x` does not live long enough
|
||||
|
||||
```rust,ignore
|
||||
{
|
||||
let r; // -------+-- 'a
|
||||
// |
|
||||
{ // |
|
||||
let x = 5; // -+-----+-- 'b
|
||||
r = &x; // | |
|
||||
} // -+ |
|
||||
// |
|
||||
println!("r: {}", r); // |
|
||||
// |
|
||||
// -------+
|
||||
}
|
||||
let r; // -------+-- 'a
|
||||
// |
|
||||
{ // |
|
||||
let x = 5; // -+-----+-- 'b
|
||||
r = &x; // | |
|
||||
} // -+ |
|
||||
// |
|
||||
println!("r: {}", r); // |
|
||||
} // -------+
|
||||
```
|
||||
|
||||
<span class="caption">示例 10-19:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b`</span>
|
||||
@ -78,7 +76,7 @@ line and ends with the first closing curly brace on the 7th line. Do you think
|
||||
the text art comments work or should we make an SVG diagram that has nicer
|
||||
looking arrows and labels? /Carol -->
|
||||
|
||||
我们将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 `r` 拥有声明周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存活的时间更短。
|
||||
我们将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 `r` 拥有声明周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存在的时间更短。
|
||||
|
||||
让我们看看示例 10-20 中这个并没有产生悬垂引用且可以正确编译的例子:
|
||||
|
||||
@ -204,15 +202,15 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
|
||||
<span class="caption">示例 10-23:`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a`</span>
|
||||
|
||||
这段代码能够编译并会产生我们想要使用示例 10-21 中的 `main` 函数得到的结果。
|
||||
这段代码能够编译并会产生我们希望得到的示例 10-21 中的 `main` 函数的结果。
|
||||
|
||||
现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。
|
||||
现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的契约。
|
||||
|
||||
我们并不会通过“在函数签名中指定生命周期参数”这种方式来改变任何参数或返回值的生命周期,而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。这个函数并不知道(或需要知道)`x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。
|
||||
通过在函数签名中指定生命周期参数,我们并没有改变任何传入后返回的值的生命周期,而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。这个函数并不知道(或需要知道)`x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。
|
||||
|
||||
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说通常是不可能分析的。在这种情况下,我们需要自己标注生命周期。
|
||||
|
||||
当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` 和 `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在 `x` 和 `y` 中较短的那个生命周期结束之前保持有效。
|
||||
当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` 和 `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数 `'a` 标注了返回的引用值,所以返回的引用值就能保证在 `x` 和 `y` 中较短的那个生命周期结束之前保持有效。
|
||||
|
||||
让我们看看如何通过传递拥有不同具体生命周期的引用来限制 `longest` 函数的使用。示例 10-24 是一个应该在任何编程语言中都很直观的例子:`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`:
|
||||
|
||||
@ -240,7 +238,7 @@ fn main() {
|
||||
|
||||
<span class="caption">示例 10-24:通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数</span>
|
||||
|
||||
接下来,让我们尝试一个 `result` 的引用的生命周期肯定比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,但是将 `result` 和 `string2` 变量的赋值语句一同放在内部作用域里。接下来,我们将使用 `result` 的 `println!` 移动到内部作用域之外,就在其结束之后。注意示例 10-25 中的代码不能编译:
|
||||
接下来,让我们尝试一个 `result` 的引用的生命周期肯定比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域里。接下来,我们将使用 `result` 的 `println!` 移动到内部作用域之外,就在其结束之后。注意示例 10-25 中的代码不能编译:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -274,7 +272,7 @@ error: `string2` does not live long enough
|
||||
|
||||
错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`。
|
||||
|
||||
以我们的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-25 中的代码,因为它可能会存在无效的引用。
|
||||
以人类的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-25 中的代码,因为它可能会存在无效的引用。
|
||||
|
||||
请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确!
|
||||
|
||||
@ -350,7 +348,7 @@ fn main() {
|
||||
|
||||
这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。
|
||||
|
||||
### 生命周期省略
|
||||
### 生命周期省略(Lifetime Elision)
|
||||
|
||||
在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的 “字符串 slice” 部分有一个函数,我们在示例 10-27 中再次展示出来,它没有生命周期注解却能成功编译:
|
||||
|
||||
@ -372,7 +370,7 @@ fn first_word(s: &str) -> &str {
|
||||
|
||||
<span class="caption">示例 10-27:第四章定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用</span>
|
||||
|
||||
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
|
||||
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 pre-1.0 版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
|
||||
|
||||
```rust,ignore
|
||||
fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
@ -392,9 +390,9 @@ fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
|
||||
1. 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。
|
||||
|
||||
2. 如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。
|
||||
2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。
|
||||
|
||||
3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self` 或 `&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法写起来更简洁。
|
||||
3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self` 或 `&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法编写起来更简洁。
|
||||
|
||||
假设我们自己就是编译器并来计算示例 10-25 `first_word` 函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:
|
||||
|
||||
@ -414,7 +412,7 @@ fn first_word<'a>(s: &'a str) -> &str {
|
||||
fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
```
|
||||
|
||||
现在这个函数签名中的所有引用都有了生命周期,而编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。
|
||||
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。
|
||||
|
||||
让我们再看看另一个例子,这次我们从示例 10-22 中没有生命周期参数的 `longest` 函数开始:
|
||||
|
||||
@ -422,13 +420,13 @@ fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
fn longest(x: &str, y: &str) -> &str {
|
||||
```
|
||||
|
||||
再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个生命周期:
|
||||
再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:
|
||||
|
||||
```rust,ignore
|
||||
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
|
||||
```
|
||||
|
||||
再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译示例 10-22 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,不过仍然不能计算出签名中所有引用的生命周期。
|
||||
再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译示例 10-22 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,不过仍不能计算出签名中所有引用的生命周期。
|
||||
|
||||
因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。
|
||||
|
||||
@ -498,7 +496,7 @@ let s: &'static str = "I have a static lifetime.";
|
||||
|
||||
### 结合泛型类型参数、trait bounds 和生命周期
|
||||
|
||||
让我们简单的看一下在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法!
|
||||
让我们简要的看一下在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法!
|
||||
|
||||
```rust
|
||||
use std::fmt::Display;
|
||||
|
@ -2,20 +2,20 @@
|
||||
|
||||
> [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-00-testing.md)
|
||||
> <br>
|
||||
> commit 1047433147b27d19e5acc068a0ebca5782d64f99
|
||||
> commit 4464eab0892297b83db7134b7ace12762a89b389
|
||||
|
||||
> Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.
|
||||
>
|
||||
> Edsger W. Dijkstra, "The Humble Programmer" (1972)
|
||||
>
|
||||
> 软件测试是证明 bug 存在的有效方法,而证明它们不存在时则显得令人绝望的不足。
|
||||
> 软件测试是证明 bug 存在的有效方法,而证明其不存在时则显得令人绝望的不足。
|
||||
>
|
||||
> Edsger W. Dijkstra,【谦卑的程序员】(1972)
|
||||
|
||||
程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。
|
||||
这并不意味着我们不该尽可能测试软件!程序的正确性意味着代码如我们期望的那样运行。Rust 是一个相当注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。
|
||||
|
||||
例如,我们可以编写一个叫做 `add_two` 的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的类型检查和借用检查,例如,这些检查会确保我们不会传递 `String` 或无效的引用给这个函数。Rust 所 **不能** 检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。
|
||||
|
||||
我们可以编写测试断言,比如说,当传递 `3` 给 `add_two` 函数时,应该得到 `5`。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。
|
||||
我们可以编写测试断言,比如说,当传递 `3` 给 `add_two` 函数时,返回值是 `5`。无论何时对代码进行修改,都可以运行测试来确保任何现存的正确行为没有被改变。
|
||||
|
||||
测试是一项复杂的技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容,所以这里仅仅讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
|
||||
测试是一项复杂的技能:虽然不能在一本书的一个章节中就涉及到编写好的测试的所有细节,我们还是会讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
|
@ -1,10 +1,17 @@
|
||||
## 编写测试
|
||||
## 如何编写测试
|
||||
|
||||
> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-01-writing-tests.md)
|
||||
> <br>
|
||||
> commit db08b34db5f1c78b4866b391c802344ec94ecc38
|
||||
> commit 4464eab0892297b83db7134b7ace12762a89b389
|
||||
|
||||
测试用来验证非测试的代码是否按照期望的方式运行的 Rust 函数。测试函数体通常包括一些设置,运行需要测试的代码,接着断言其结果是我们所期望的。让我们看看 Rust 提供的专门用来编写测试的功能:`test` 属性、一些宏和 `should_panic` 属性。
|
||||
测试用来验证非测试的代码是否按照期望的方式运行的 Rust 函数。测试函数体通常执行如下三种操作:
|
||||
|
||||
1. 设置任何所需的数据或状态
|
||||
2. 运行需要测试的代码
|
||||
3. 断言其结果是我们所期望的
|
||||
|
||||
|
||||
让我们看看 Rust 提供的专门用来编写测试的功能:`test` 属性、一些宏和 `should_panic` 属性。
|
||||
|
||||
### 测试函数剖析
|
||||
|
||||
@ -12,7 +19,7 @@
|
||||
|
||||
第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然也可以额外增加任意多的测试函数以及测试模块!
|
||||
|
||||
我们将先通过对自动生成的测试模板做一些试验来探索测试如何工作方面的一些内容,而不实际测试任何代码。接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是否正确。
|
||||
我们将先通过对自动生成的测试模板做一些试验来探索一些测试如何工作方面的内容,而不实际测试任何代码。接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是否正确。
|
||||
|
||||
让我们创建一个新的库项目 `adder`:
|
||||
|
||||
@ -22,7 +29,7 @@ $ cargo new adder
|
||||
$ cd adder
|
||||
```
|
||||
|
||||
adder 库中 `src/lib.rs` 的内容应该看起来像这样:
|
||||
adder 库中 `src/lib.rs` 的内容应该看起来如示例 11-1 所示:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -31,15 +38,16 @@ adder 库中 `src/lib.rs` 的内容应该看起来像这样:
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<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。这个断言作为一个典型测试格式的例子。让我们运行以便看到测试通过。
|
||||
|
||||
`cargo test` 命令会运行项目中所有的测试,如示例 11-2 所示:
|
||||
|
||||
@ -52,32 +60,35 @@ $ cargo test
|
||||
running 1 test
|
||||
test tests::it_works ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
Doc-tests adder
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
<span class="caption">示例 11-2:运行自动生成测试的输出</span>
|
||||
|
||||
Cargo 编译并运行了测试。在 `Compiling`、`Finished` 和 `Running` 这几行之后,可以看到 `running 1 test` 这一行。下一行显示了生成的测试函数的名称,它是 `it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的总结:`test result: ok.` 意味着所有测试都通过了。`1 passed; 0 failed` 表示通过或失败的测试数量。
|
||||
|
||||
这里并没有任何被标记为忽略的测试,所以总结表明 `0 ignored`。在下一部分关于运行测试的不同方式中会讨论忽略测试。`0 measured` 统计是针对测试性能的性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。请查看附录 D 来了解更多 Rust 开发版的信息。
|
||||
这里并没有任何被标记为忽略的测试,所以总结表明 `0 ignored`。我们也没有过滤需要运行的测试,所以总结的结尾显示`0 filtered out`。在下一部分 “控制测试如何运行” 会讨论忽略和过滤测试。
|
||||
|
||||
`0 measured` 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。请查看第一章来了解更多 Rust 开发版的信息。
|
||||
|
||||
测试输出中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。
|
||||
|
||||
让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn exploration() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -88,10 +99,10 @@ mod tests {
|
||||
running 1 test
|
||||
test tests::exploration ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏!写入新函数后 `src/lib.rs` 现在看起来如示例 11-3 所示:
|
||||
让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏。写入新测试 `another` 后, src/lib.rs` 现在看起来如示例 11-3 所示:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -100,6 +111,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
mod tests {
|
||||
#[test]
|
||||
fn exploration() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -109,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` 失败了:
|
||||
@ -122,30 +134,30 @@ test tests::another ... FAILED
|
||||
failures:
|
||||
|
||||
---- tests::another stdout ----
|
||||
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:9
|
||||
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:8
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
|
||||
failures:
|
||||
tests::another
|
||||
|
||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
error: test failed
|
||||
```
|
||||
|
||||
<span class="caption">示例 11-4:一个测试通过和一个测试失败的测试结果</span>
|
||||
|
||||
`test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为 `panicked at 'Make this test fail'` 而失败,这位于 *src/lib.rs* 的第 9 行。下一部分仅仅列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。
|
||||
`test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为 `panicked at 'Make this test fail'` 而失败,这位于 *src/lib.rs* 的第 10 行。下一部分仅仅列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分 “控制测试如何运行” 会讲到更多运行测试的方法。
|
||||
|
||||
最后是总结行:总体上讲,一个测试结果是 `FAILED` 的。有一个测试通过和一个测试失败。
|
||||
最后是总结行:总体上讲,测试结果是 `FAILED`。有一个测试通过和一个测试失败。
|
||||
|
||||
现在我们见过不同场景中测试结果是什么样子的了,再来看看除 `panic!` 之外的一些在测试中有帮助的宏吧。
|
||||
|
||||
### 使用 `assert!` 宏来检查结果
|
||||
|
||||
`assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个计算为布尔值的参数。如果值是 `true`,`assert!` 什么也不做同时测试会通过。如果值为 `false`,`assert!` 调用 `panic!` 宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。
|
||||
`assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个计算为布尔值的参数。如果值是 `true`,`assert!` 什么也不做同时测试会通过。如果值为 `false`,`assert!` 调用 `panic!` 宏,这会导致测试失败。`assert!` 宏帮助我们检查代码是否以期望的方式运行。
|
||||
|
||||
回忆一下第五章中,示例 5-9 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用他们。将他们放进 *src/lib.rs* 而不是 *src/main.rs* 并使用 `assert!` 宏编写一些测试。
|
||||
回忆一下第五章中,示例 5-15 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用他们。将他们放进 *src/lib.rs* 并使用 `assert!` 宏编写一些测试。
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -186,7 +198,7 @@ mod tests {
|
||||
|
||||
<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`,所以测试应该通过。让我们拭目以待!
|
||||
|
||||
@ -194,7 +206,7 @@ mod tests {
|
||||
running 1 test
|
||||
test tests::larger_can_hold_smaller ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
它确实通过了!再来增加另一个测试,这一回断言一个更小的矩形不能放下一个更大的矩形:
|
||||
@ -208,10 +220,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn larger_can_hold_smaller() {
|
||||
let larger = Rectangle { length: 8, width: 7 };
|
||||
let smaller = Rectangle { length: 5, width: 1 };
|
||||
|
||||
assert!(larger.can_hold(&smaller));
|
||||
// --snip--
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -231,17 +240,18 @@ running 2 tests
|
||||
test tests::smaller_cannot_hold_larger ... ok
|
||||
test tests::larger_can_hold_smaller ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
两个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将 `can_hold` 方法中比较长度时本应使用大于号的地方改成小于号:
|
||||
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct Rectangle {
|
||||
length: u32,
|
||||
width: u32,
|
||||
}
|
||||
# #[derive(Debug)]
|
||||
# pub struct Rectangle {
|
||||
# length: u32,
|
||||
# width: u32,
|
||||
# }
|
||||
// --snip--
|
||||
|
||||
impl Rectangle {
|
||||
pub fn can_hold(&self, other: &Rectangle) -> bool {
|
||||
@ -260,21 +270,21 @@ test tests::larger_can_hold_smaller ... FAILED
|
||||
failures:
|
||||
|
||||
---- tests::larger_can_hold_smaller stdout ----
|
||||
thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed:
|
||||
larger.can_hold(&smaller)', src/lib.rs:22
|
||||
thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed:
|
||||
larger.can_hold(&smaller)', src/lib.rs:22:8
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
|
||||
failures:
|
||||
tests::larger_can_hold_smaller
|
||||
|
||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
我们的测试捕获了 bug!因为 `larger.length` 是 8 而 `smaller.length` 是 5,`can_hold` 中的长度比较现在因为 8 不小于 5 而返回 `false`。
|
||||
|
||||
### 使用 `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!` 宏测试这个函数:
|
||||
|
||||
@ -304,7 +314,7 @@ mod tests {
|
||||
running 1 test
|
||||
test tests::it_adds_two ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
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` 表明测试通过了!
|
||||
@ -326,23 +336,24 @@ test tests::it_adds_two ... FAILED
|
||||
failures:
|
||||
|
||||
---- tests::it_adds_two stdout ----
|
||||
thread 'tests::it_adds_two' panicked at 'assertion failed: `(left ==
|
||||
right)` (left: `4`, right: `5`)', src/lib.rs:11
|
||||
thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)`
|
||||
left: `4`,
|
||||
right: `5`', src/lib.rs:11:8
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
|
||||
failures:
|
||||
tests::it_adds_two
|
||||
|
||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||||
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_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是可推导 trait,如第五章所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C 中有更多关于这些和其他可推导 trait 的详细信息。
|
||||
`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C 中有更多关于这些和其他派生 trait 的详细信息。
|
||||
|
||||
### 自定义错误信息
|
||||
|
||||
@ -350,7 +361,7 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||||
|
||||
例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
pub fn greeting(name: &str) -> String {
|
||||
@ -369,8 +380,7 @@ mod tests {
|
||||
}
|
||||
```
|
||||
|
||||
这个程序的需求还没有被确定,而我们非常确定问候开始的 `Hello` 文本不会改变。我们决定并不想在人名改变时
|
||||
不得不更新测试,所以相比检查 `greeting` 函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。
|
||||
这个程序的需求还没有被确定,而我们非常确定问候开始的 `Hello` 文本不会改变。我们决定并不想在人名改变时不得不更新测试,所以相比检查 `greeting` 函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。
|
||||
|
||||
让我们通过将 `greeting` 改为不包含 `name` 来在代码中引入一个 bug 来测试失败时是怎样的,
|
||||
|
||||
@ -389,8 +399,8 @@ test tests::greeting_contains_name ... FAILED
|
||||
failures:
|
||||
|
||||
---- tests::greeting_contains_name stdout ----
|
||||
thread 'tests::greeting_contains_name' panicked at 'assertion failed:
|
||||
result.contains("Carol")', src/lib.rs:12
|
||||
thread 'tests::greeting_contains_name' panicked at 'assertion failed:
|
||||
result.contains("Carol")', src/lib.rs:12:8
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
|
||||
failures:
|
||||
@ -412,18 +422,19 @@ fn greeting_contains_name() {
|
||||
|
||||
现在如果再次运行测试,将会看到更有价值的错误信息:
|
||||
|
||||
|
||||
```text
|
||||
---- tests::greeting_contains_name stdout ----
|
||||
thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain
|
||||
name, value was `Hello`', src/lib.rs:12
|
||||
thread 'tests::greeting_contains_name' panicked at 'Greeting did not
|
||||
contain name, value was `Hello!`', src/lib.rs:12:8
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
```
|
||||
|
||||
可以在测试输出中看到所取得的确切的值,这会帮助我们理解发生了什么而不是期望发生什么。
|
||||
可以在测试输出中看到所取得的确切的值,这会帮助我们理解真正发生了什么而不是期望发生什么。
|
||||
|
||||
### 使用 `should_panic` 检查 panic
|
||||
|
||||
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章示例 9-8 创建的 `Guess` 类型。其他使用 `Guess` 的代码依赖于 `Guess` 实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。
|
||||
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章示例 9-9 创建的 `Guess` 类型。其他使用 `Guess` 的代码依赖于 `Guess` 实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。
|
||||
|
||||
可以通过对函数增加另一个属性 `should_panic` 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。
|
||||
|
||||
@ -432,7 +443,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
struct Guess {
|
||||
pub struct Guess {
|
||||
value: u32,
|
||||
}
|
||||
|
||||
@ -468,16 +479,18 @@ mod tests {
|
||||
running 1 test
|
||||
test tests::greater_than_100 ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
看起来不错!现在在代码中引入 bug,移除 `new` 函数在值大于 100 时会 panic 的条件:
|
||||
|
||||
```rust
|
||||
# struct Guess {
|
||||
# pub struct Guess {
|
||||
# value: u32,
|
||||
# }
|
||||
#
|
||||
// --snip--
|
||||
|
||||
impl Guess {
|
||||
pub fn new(value: u32) -> Guess {
|
||||
if value < 1 {
|
||||
@ -502,7 +515,7 @@ failures:
|
||||
failures:
|
||||
tests::greater_than_100
|
||||
|
||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了 `#[should_panic]`。这个错误意味着代码中函数 `Guess::new(200)` 并没有产生 panic。
|
||||
@ -512,9 +525,11 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
struct Guess {
|
||||
value: u32,
|
||||
}
|
||||
# pub struct Guess {
|
||||
# value: u32,
|
||||
# }
|
||||
#
|
||||
// --snip--
|
||||
|
||||
impl Guess {
|
||||
pub fn new(value: u32) -> Guess {
|
||||
@ -567,8 +582,8 @@ test tests::greater_than_100 ... FAILED
|
||||
failures:
|
||||
|
||||
---- tests::greater_than_100 stdout ----
|
||||
thread 'tests::greater_than_100' panicked at 'Guess value must be greater
|
||||
than or equal to 1, got 200.', src/lib.rs:10
|
||||
thread 'tests::greater_than_100' panicked at 'Guess value must be
|
||||
greater than or equal to 1, got 200.', src/lib.rs:11:12
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
note: Panic did not include expected string 'Guess value must be less than or
|
||||
equal to 100'
|
||||
@ -576,9 +591,9 @@ equal to 100'
|
||||
failures:
|
||||
tests::greater_than_100
|
||||
|
||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||||
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 在哪了!
|
||||
|
||||
现在我们讲完了编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于 `cargo test` 的不同选项。
|
||||
现在你知道了几种编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于 `cargo test` 的不同选项。
|
@ -1,16 +1,16 @@
|
||||
## 运行测试
|
||||
## 控制测试如何运行
|
||||
|
||||
> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-02-running-tests.md)
|
||||
> <br>
|
||||
> commit db08b34db5f1c78b4866b391c802344ec94ecc38
|
||||
> 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` 则会告诉你位于分隔符 `--` 之后的相关参数。
|
||||
|
||||
### 并行或连续的运行测试
|
||||
|
||||
当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或依赖任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境。
|
||||
当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或依赖任何共享状态,这包括类似于当前工作目录或者环境变量这样的共享环境。
|
||||
|
||||
例如,每一个测试都运行一些代码在硬盘上创建一个 `test-output.txt` 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。
|
||||
|
||||
@ -26,7 +26,7 @@ $ cargo test -- --test-threads=1
|
||||
|
||||
如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用 `println!` 而测试通过了,我们将不会在终端看到 `println!` 的输出:只会看到说明测试通过的行。如果测试失败了,就会看到所有标准输出和其他错误信息。
|
||||
|
||||
例如,示例 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:
|
||||
例如,示例 11-10 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -66,15 +66,16 @@ test tests::this_test_will_fail ... FAILED
|
||||
failures:
|
||||
|
||||
---- tests::this_test_will_fail stdout ----
|
||||
I got the value 8
|
||||
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
|
||||
right)` (left: `5`, right: `10`)', src/lib.rs:19
|
||||
I got the value 8
|
||||
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
|
||||
left: `5`,
|
||||
right: `10`', src/lib.rs:19:8
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
|
||||
failures:
|
||||
tests::this_test_will_fail
|
||||
|
||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
注意输出中哪里也不会出现 `I got the value 4`,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,`I got the value 8`,则出现在输出的测试总结部分,同时也显示了测试失败的原因。
|
||||
@ -85,15 +86,16 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||||
$ cargo test -- --nocapture
|
||||
```
|
||||
|
||||
使用 `--nocapture` 参数再次运行示例 11-10 中的测试会显示:
|
||||
使用 `--nocapture` 参数再次运行示例 11-10 中的测试会显示如下输出:
|
||||
|
||||
```text
|
||||
running 2 tests
|
||||
I got the value 4
|
||||
I got the value 8
|
||||
test tests::this_test_will_pass ... ok
|
||||
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
|
||||
right)` (left: `5`, right: `10`)', src/lib.rs:19
|
||||
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
|
||||
left: `5`,
|
||||
right: `10`', src/lib.rs:19:8
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
test tests::this_test_will_fail ... FAILED
|
||||
|
||||
@ -102,16 +104,16 @@ failures:
|
||||
failures:
|
||||
tests::this_test_will_fail
|
||||
|
||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用`--test-threads=1`和`--nocapture`功能来看看输出是什么样子!
|
||||
注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用 `--test-threads=1` 和 `--nocapture` 功能来看看输出是什么样子!
|
||||
|
||||
### 通过名称来运行测试的子集
|
||||
|
||||
有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这些代码相关的测试。可以向 `cargo test` 传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。
|
||||
|
||||
为了展示如何运行测试的子集,示例 11-11 使用 `add_two` 函数创建了三个测试来供我们选择运行哪一个:
|
||||
为了展示如何运行测试的子集,示例 11-11 为 `add_two` 函数创建了三个测试来供我们选择运行哪一个:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -151,7 +153,7 @@ test tests::add_two_and_two ... ok
|
||||
test tests::add_three_and_two ... ok
|
||||
test tests::one_hundred ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
#### 运行单个测试
|
||||
@ -166,10 +168,12 @@ $ cargo test one_hundred
|
||||
running 1 test
|
||||
test tests::one_hundred ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
|
||||
```
|
||||
|
||||
不能像这样指定多个测试名称,只有传递给 `cargo test` 的第一个值才会被使用。
|
||||
只有名称为 `one_hundred` 的测试被运行了;其余两个测试并不匹配这个名称。测试输出在总结行的结尾显示了 `2 filtered out` 表明存在比本命令所运行的更多的测试。
|
||||
|
||||
不能像这样指定多个测试名称,只有传递给 `cargo test` 的第一个值才会被使用。不过有运行多个测试的方法。
|
||||
|
||||
#### 过滤运行多个测试
|
||||
|
||||
@ -184,21 +188,21 @@ running 2 tests
|
||||
test tests::add_two_and_two ... ok
|
||||
test tests::add_three_and_two ... ok
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
|
||||
```
|
||||
|
||||
这运行了所有名字中带有 `add` 的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。
|
||||
|
||||
### 除非指定否则忽略某些测试
|
||||
|
||||
有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 `cargo test` 的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用 `ignore` 属性来标记耗时的测试来排除他们:
|
||||
有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 `cargo test` 的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用 `ignore` 属性来标记耗时的测试并排除他们,如下所示:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true);
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -220,16 +224,10 @@ running 2 tests
|
||||
test expensive_test ... ignored
|
||||
test it_works ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured
|
||||
|
||||
Doc-tests adder
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||||
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
|
||||
$ cargo test -- --ignored
|
||||
@ -239,7 +237,7 @@ $ cargo test -- --ignored
|
||||
running 1 test
|
||||
test expensive_test ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
|
||||
```
|
||||
|
||||
通过控制运行哪些测试,可以确保运行 `cargo test` 的结果是快速的。当某个时刻需要检查 `ignored` 测试的结果而且你也有时间等待这个结果的话,可以选择执行 `cargo test -- --ignored`。
|
@ -2,9 +2,9 @@
|
||||
|
||||
> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-03-test-organization.md)
|
||||
> <br>
|
||||
> commit 0665bc5646339a6bcda21838f46d4357b9435e75
|
||||
> commit b3eddb8edc0c3f83647143673d18efac0a44083a
|
||||
|
||||
正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与 **集成测试**(*integration tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块,也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。
|
||||
正如之前提到的,测试是一个复杂的概念,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与 **集成测试**(*integration tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块,也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户采用相同的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。
|
||||
|
||||
编写这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
|
||||
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位代码位于何处和是否符合预期。单元测试位于 *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 为我们生成了如下代码:
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -63,13 +64,13 @@ mod tests {
|
||||
|
||||
### 集成测试
|
||||
|
||||
在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 *tests* 目录。
|
||||
在 Rust 中,集成测试对于需要测试的库来完全说是外部的。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 *tests* 目录。
|
||||
|
||||
#### *tests* 目录
|
||||
|
||||
为了编写集成测试,需要在项目根目录创建一个 *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>
|
||||
|
||||
@ -84,12 +85,12 @@ fn it_adds_two() {
|
||||
|
||||
<span class="caption">示例 11-13:一个 `adder` crate 中函数的集成测试</span>
|
||||
|
||||
我们在顶部增加了 `extern crate adder`,这在单元测试中是不需要的。这是因为每一个 `tests` 目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。集成测试就像其他库使用者那样通过导入 crate 并只使用公有 API。
|
||||
我们在顶部增加了 `extern crate adder`,这在单元测试中是不需要的。这是因为每一个 `tests` 目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。
|
||||
|
||||
并不需要将 *tests/integration_test.rs* 中的任何代码标注为 `#[cfg(test)]`。Cargo 对 `tests` 文件夹特殊处理并只会在运行 `cargo test` 时编译这个目录中的文件。现在就试试运行 `cargo test`:
|
||||
并不需要将 *tests/integration_test.rs* 中的任何代码标注为 `#[cfg(test)]`。Cargo 对 `tests` 文件夹特殊处理并只会在运行 `cargo test` 时编译这个目录中的文件。现在就运行 `cargo test` 试试:
|
||||
|
||||
```text
|
||||
cargo test
|
||||
$ cargo test
|
||||
Compiling adder v0.1.0 (file:///projects/adder)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
|
||||
Running target/debug/deps/adder-abcabcabc
|
||||
@ -97,20 +98,20 @@ cargo test
|
||||
running 1 test
|
||||
test tests::internal ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
Running target/debug/deps/integration_test-ce99bcc2479f4607
|
||||
|
||||
running 1 test
|
||||
test it_adds_two ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
Doc-tests adder
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(示例 11-12 中有一个叫做 `internal` 的测试),接着是一个单元测试的总结行。
|
||||
@ -129,18 +130,18 @@ $ cargo test --test integration_test
|
||||
running 1 test
|
||||
test it_adds_two ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
这些只是 *tests* 目录中我们指定的文件中的测试。
|
||||
|
||||
#### 集成测试中的子模块
|
||||
|
||||
随着集成测试的增加,你可能希望在 `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>
|
||||
|
||||
@ -156,31 +157,31 @@ pub fn setup() {
|
||||
running 1 test
|
||||
test tests::internal ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
Running target/debug/deps/common-b8b07b6f1be2db70
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
Running target/debug/deps/integration_test-d993c68b431d39df
|
||||
|
||||
running 1 test
|
||||
test it_adds_two ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
Doc-tests adder
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
`common` 出现在测试结果中并显示 `running 0 tests`,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。
|
||||
|
||||
为了使 `common` 不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建 *tests/common.rs*,而是创建 *tests/common/mod.rs*。当将 `setup` 代码移动到 *tests/common/mod.rs* 并去掉 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。
|
||||
为了避免 `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 编译或作为一部分出现在测试输出中。
|
||||
|
||||
一旦拥有了 *tests/common/mod.rs*,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 *tests/integration_test.rs* 中调用 `setup` 函数的 `it_adds_two` 测试的例子:
|
||||
|
||||
@ -198,7 +199,7 @@ fn it_adds_two() {
|
||||
}
|
||||
```
|
||||
|
||||
注意 `mod common;` 声明与第七章中的模块声明相同。接着在测试函数中就可以调用 `common::setup()` 了。
|
||||
注意 `mod common;` 声明与示例 7-4 中展示的模块声明相同。接着在测试函数中就可以调用 `common::setup()` 了。
|
||||
|
||||
#### 二进制 crate 的集成测试
|
||||
|
||||
@ -208,6 +209,6 @@ fn it_adds_two() {
|
||||
|
||||
## 总结
|
||||
|
||||
Rust 的测试功能提供了一个如何确保即使函数做出改变也能继续以指定方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他外部代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 仍然是很重要的。
|
||||
Rust 的测试功能提供了一个如何确保即使函数做出改变也能继续以期望的方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他外部代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 仍然是很重要的。
|
||||
|
||||
接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-00-an-io-project.md)
|
||||
> <br>
|
||||
> commit 096db3d06b25692ee701750e1f995ba00d0f5c55
|
||||
> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1
|
||||
|
||||
本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。
|
||||
|
||||
Rust 的运行速度、安全性、**单二进制文件** 输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件名和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。
|
||||
Rust 的运行速度、安全性、**单二进制文件** 输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件名和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行。然后打印出这些行。
|
||||
|
||||
在这个过程中,我们会展示如何让我们的命令行工具利用很多命令行工具中用到的终端功能。读取环境变量来使得用户可以配置工具的行为。打印到标准错误控制流(`stderr`) 而不是标准输出(`stdout`),例如这样用户可以选择将成功输出重定向到文件中而仍然在屏幕上显示错误信息。
|
||||
|
||||
一位 Rust 社区的成员,Andrew Gallant,已经创建了一个功能完整且非常快速的 `grep` 版本,叫做 [`ripgrep`](https://github.com/BurntSushi/ripgrep)<!-- ignore -->。相比之下,我们的 `grep` 版本将非常简单,本章将教会你一些帮助理解像 `ripgrep` 这样真实项目的背景知识。
|
||||
一位 Rust 社区的成员,Andrew Gallant,已经创建了一个功能完整且非常快速的 `grep` 版本,叫做 `ripgrep`。相比之下,我们的 `grep` 版本将非常简单,本章将教会你一些帮助理解像 `ripgrep` 这样真实项目的背景知识。
|
||||
|
||||
这个项目将会结合之前所学的一些内容:
|
||||
我们的 `grep` 项目将会结合之前所学的一些内容:
|
||||
|
||||
- 代码组织(使用第七章学习的模块)
|
||||
- vector 和字符串(第八章,集合)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
> [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-01-accepting-command-line-arguments.md)
|
||||
> <br>
|
||||
> commit 50658e654fb6a9208b635179cdd79939aa0ab133
|
||||
> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1
|
||||
|
||||
一如之前使用 `cargo new` 新建一个项目。我们称之为 `minigrep` 以便与可能已经安装在系统上的`grep`工具相区别:
|
||||
|
||||
@ -18,13 +18,13 @@ $ cd minigrep
|
||||
$ cargo run searchstring example-filename.txt
|
||||
```
|
||||
|
||||
现在 `cargo new` 生成的程序忽略任何传递给它的参数。crates.io 上有一些现成的库可以帮助我们接受命令行参数,不过因为正在学习,让我们自己来实现一个。
|
||||
现在 `cargo new` 生成的程序忽略任何传递给它的参数。[Crates.io](https://crates.io/) 上有一些现成的库可以帮助我们接受命令行参数,不过因为正在学习,让我们自己来实现一个。
|
||||
|
||||
### 读取参数值
|
||||
|
||||
首先我们需要程序能够获取传递给它的命令行参数的值,为此需要一个 Rust 标准库提供的函数:`std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器**(*iterator*)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个 vector,比如包含所有迭代器产生元素的 vector。
|
||||
为了确保 `minigrep` 能够获取传递给它的命令行参数的值,我们需要一个 Rust 标准库提供的函数,也就是 `std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器**(*iterator*)。我们还未讨论到迭代器(第十三章会全面的介绍他们),但是现在只需理解迭代器的两个细节:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个集合,比如包含所有迭代器产生元素的 vector。
|
||||
|
||||
让我们尝试一下:使用示例 12-1 中的代码来读取任何传递给 `minigrep` 的命令行参数并将其收集到一个 vector 中。
|
||||
使用示例 12-1 中的代码来读取任何传递给 `minigrep` 的命令行参数并将其收集到一个 vector 中。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -37,13 +37,14 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
示例 12-1:将命令行参数收集到一个 vector 中并打印出来
|
||||
|
||||
首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些;这样容易被错认成一个定义于当前模块的函数。
|
||||
<span class="caption">示例 12-1:将命令行参数收集到一个 vector 中并打印出来</span>
|
||||
|
||||
首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些,因为 `args` 容易被错认成一个定义于当前模块的函数。
|
||||
|
||||
> ### `args` 函数和无效的 Unicode
|
||||
>
|
||||
> 注意 `std::env::args` 在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用 `std::env::args_os` 代替。这个函数返回 `OsString` 值而不是 `String` 值。这里出于简单考虑使用了 `std::env::args`,因为 `OsString` 值每个平台都不一样而且比 `String` 值处理起来更复杂。
|
||||
> 注意 `std::env::args` 在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用 `std::env::args_os` 代替。这个函数返回 `OsString` 值而不是 `String` 值。这里出于简单考虑使用了 `std::env::args`,因为 `OsString` 值每个平台都不一样而且比 `String` 值处理起来更为复杂。
|
||||
|
||||
在 `main` 函数的第一行,我们调用了 `env::args`,并立即使用 `collect` 来创建了一个包含迭代器所有值的 vector。`collect` 可以被用来创建很多类型的集合,所以这里显式注明 `args` 的类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,`collect` 就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。
|
||||
|
||||
@ -51,18 +52,19 @@ fn main() {
|
||||
|
||||
```text
|
||||
$ cargo run
|
||||
--snip--
|
||||
["target/debug/minigrep"]
|
||||
|
||||
$ cargo run needle haystack
|
||||
...snip...
|
||||
--snip--
|
||||
["target/debug/minigrep", "needle", "haystack"]
|
||||
```
|
||||
|
||||
你可能注意到了 vector 的第一个值是 `"target/debug/minigrep"`,它是我们二进制文件的名称。这与 C 中的参数列表的行为相符合,并使得程序可以在执行过程中使用它的名字。能够访问程序名称在需要在信息中打印时,或者需要根据执行程序所使用的命令行别名来改变程序行为时显得很方便,不过考虑到本章的目的,我们将忽略它并只保存所需的两个参数。
|
||||
注意 vector 的第一个值是 `"target/debug/minigrep"`,它是我们二进制文件的名称。这与 C 中的参数列表的行为相符合,并使得程序可以在执行过程中使用它的名字。能够访问程序名称在需要在信息中打印时,或者需要根据执行程序所使用的命令行别名来改变程序行为时显得很方便,不过考虑到本章的目的,我们将忽略它并只保存所需的两个参数。
|
||||
|
||||
### 将参数值保存进变量
|
||||
|
||||
打印出参数 vector 中的值展示了程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值。让我们如示例 12-2 这样做:
|
||||
打印出参数 vector 中的值展示了程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值了。让我们如示例 12-2 这样做:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -80,14 +82,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
示例 12-2:创建变量来存放查询参数和文件名参数
|
||||
<span class="caption">示例 12-2:创建变量来存放查询参数和文件名参数</span>
|
||||
|
||||
正如之前打印出 vector 时所所看到的,程序的名称占据了 vector 的第一个值 `args[0]`,所以我们从索引 `1` 开始。`minigrep` 获取的第一个参数是需要搜索的字符串,所以将其将第一个参数的引用存放在变量 `query` 中。第二个参数将是文件名,所以将第二个参数的引用放入变量 `filename` 中。
|
||||
|
||||
我们将临时打印出这些变量的值,再一次证明代码如我们期望的那样工作。让我们使用参数 `test` 和 `sample.txt` 再次运行这个程序:
|
||||
我们将临时打印出这些变量的值来证明代码如我们期望的那样工作。使用参数 `test` 和 `sample.txt` 再次运行这个程序:
|
||||
|
||||
```text
|
||||
$ cargo run test sample.txt
|
||||
Compiling minigrep v0.1.0 (file:///projects/minigrep)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
|
||||
Running `target/debug/minigrep test sample.txt`
|
||||
Searching for test
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
> [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-02-reading-a-file.md)
|
||||
> <br>
|
||||
> commit b693c8400817f1022820fd63e3529cbecc35070c
|
||||
> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1
|
||||
|
||||
接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保 `minigrep` 正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?":
|
||||
接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保 `minigrep` 正常工作的最好的文件是拥有多行少量文本且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?":
|
||||
|
||||
<span class="filename">文件名: poem.txt</span>
|
||||
|
||||
@ -38,7 +38,7 @@ fn main() {
|
||||
# let filename = &args[2];
|
||||
#
|
||||
# println!("Searching for {}", query);
|
||||
// ...snip...
|
||||
// --snip--
|
||||
println!("In file {}", filename);
|
||||
|
||||
let mut f = File::open(filename).expect("file not found");
|
||||
@ -55,7 +55,7 @@ fn main() {
|
||||
|
||||
首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs::File` 来处理文件,而 `std::io::prelude::*` 则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io` 也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式 `use` 位于 `std::io` 中的 prelude。
|
||||
|
||||
在 `main` 中,我们增加了三点内容:第一,通过传递变量 `filename` 的值调用 `File::open` 函数来获取文件的可变句柄。创建了叫做 `contents` 的变量并将其设置为一个可变的,空的`String`。它将会存放之后读取的文件的内容。第三,对文件句柄调用 `read_to_string` 并传递 `contents` 的可变引用作为参数。
|
||||
在 `main` 中,我们增加了三点内容:第一,通过传递变量 `filename` 的值调用 `File::open` 函数来获取文件的可变句柄。创建了叫做 `contents` 的变量并将其设置为一个可变的,空的 `String`。它将会存放之后读取的文件的内容。第三,对文件句柄调用 `read_to_string` 并传递 `contents` 的可变引用作为参数。
|
||||
|
||||
在这些代码之后,我们再次增加了临时的 `println!` 打印出读取文件后 `contents` 的值,这样就可以检查目前为止的程序能否工作。
|
||||
|
||||
@ -63,6 +63,7 @@ fn main() {
|
||||
|
||||
```text
|
||||
$ cargo run the poem.txt
|
||||
Compiling minigrep v0.1.0 (file:///projects/minigrep)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
|
||||
Running `target/debug/minigrep the poem.txt`
|
||||
Searching for the
|
||||
|
Loading…
Reference in New Issue
Block a user