check ch12-01

This commit is contained in:
KaiserY 2017-08-25 16:36:49 +08:00
parent 745167431c
commit d951f6298e
5 changed files with 135 additions and 209 deletions

View File

@ -274,11 +274,11 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
### 使用 `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!` 宏测试这个函数:
<span class="filename">Filename: src/lib.rs</span>
<span class="filename">文件名: src/lib.rs</span>
```rust
pub fn add_two(a: i32) -> i32 {
@ -296,21 +296,20 @@ mod tests {
}
```
<span class="caption">Listing 11-7: Testing the function `add_two` using the
`assert_eq!` macro </span>
<span class="caption">列表 11-7使用 `assert_eq!` 宏测试 `add_two`</span>
测试通过了!
```
```text
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
```
传递给`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
```rust
pub fn add_two(a: i32) -> i32 {
@ -320,7 +319,7 @@ pub fn add_two(a: i32) -> i32 {
再次运行测试:
```
```text
running 1 test
test tests::it_adds_two ... FAILED
@ -337,17 +336,17 @@ failures:
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
```
测试捕获到了 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如第五章所提到的通常可以直接在结构体或枚举上添加`#[derive(PartialEq, Debug)]`注解。附录 C 中有更多关于这些和其他可推导 trait 的详细信息。
`assert_eq!` `assert_ne!` 宏在底层分别使用了 `==` `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是可推导 trait如第五章所提到的通常可以直接在结构体或枚举上添加 `#[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!` 宏,所以可以传递一个包含 `{}` 占位符的格式字符串和放入占位符的值。自定义信息有助于记录断言的意义,这样到测试失败时,就能更好的理解代码出了什么问题。
例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
@ -370,10 +369,10 @@ mod tests {
}
```
这个程序的需求还没有被确定,而我们非常确定问候开始的`Hello`文本不会改变。我们决定并不想在人名改变时
不得不更新测试,所以相比检查`greeting`函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。
这个程序的需求还没有被确定,而我们非常确定问候开始的 `Hello` 文本不会改变。我们决定并不想在人名改变时
不得不更新测试,所以相比检查 `greeting` 函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。
让我们通过改变`greeting`不包含`name`来在代码中引入一个 bug 来测试失败时是怎样的,
让我们通过`greeting` 改为不包含 `name` 来在代码中引入一个 bug 来测试失败时是怎样的,
```rust
pub fn greeting(name: &str) -> String {
@ -398,7 +397,7 @@ failures:
tests::greeting_contains_name
```
这仅仅告诉了我们断言失败了和失败的行号。一个更有用的错误信息应该打印出从`greeting`函数得到的值。让我们改变测试函数来使用一个由包含占位符的格式字符串和从`greeting`函数取得的值组成的自定义错误信息:
这仅仅告诉了我们断言失败了和失败的行号。一个更有用的错误信息应该打印出从 `greeting` 函数得到的值。让我们改变测试函数来使用一个由包含占位符的格式字符串和从 `greeting` 函数取得的值组成的自定义错误信息:
```rust,ignore
#[test]
@ -413,7 +412,7 @@ 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
@ -422,15 +421,15 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
可以在测试输出中看到所取得的确切的值,这会帮助我们理解发生了什么而不是期望发生什么。
### 使用`should_panic`检查 panic
### 使用 `should_panic` 检查 panic
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章列表 9-8 创建的`Guess`类型。其他使用`Guess`的代码依赖于`Guess`实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的`Guess`实例会 panic。
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章列表 9-8 创建的 `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">Filename: src/lib.rs</span>
<span class="filename">文件名: src/lib.rs</span>
```rust
struct Guess {
@ -444,7 +443,7 @@ impl Guess {
}
Guess {
value: value,
value
}
}
}
@ -461,19 +460,18 @@ mod tests {
}
```
<span class="caption">Listing 11-8: Testing that a condition will cause a
`panic!` </span>
<span class="caption">列表 11-8测试会造成 `panic!` 的条件</span>
`#[should_panic]`属性位于`#[test]`之后和对应的测试函数之前。让我们看看测试通过时它时什么样子:
`#[should_panic]` 属性位于 `#[test]` 之后和对应的测试函数之前。让我们看看测试通过时它时什么样子:
```
```text
running 1 test
test tests::greater_than_100 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
```
看起来不错!现在在代码中引入 bug通过移除`new`函数在值大于 100 时会 panic 的条件:
看起来不错!现在在代码中引入 bug移除 `new` 函数在值大于 100 时会 panic 的条件:
```rust
# struct Guess {
@ -487,7 +485,7 @@ impl Guess {
}
Guess {
value: value,
value
}
}
}
@ -495,7 +493,7 @@ impl Guess {
如果运行列表 11-8 的测试,它会失败:
```
```text
running 1 test
test tests::greater_than_100 ... FAILED
@ -507,11 +505,11 @@ failures:
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
```
这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了`#[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">Filename: src/lib.rs</span>
<span class="filename">文件名: src/lib.rs</span>
```rust
struct Guess {
@ -529,7 +527,7 @@ impl Guess {
}
Guess {
value: value,
value
}
}
}
@ -546,12 +544,11 @@ mod tests {
}
```
<span class="caption">Listing 11-9: Testing that a condition will cause a
`panic!` with a particular panic message </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.`。这依赖于 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` 的代码块对换:
```rust,ignore
if value < 1 {
@ -561,9 +558,9 @@ if value < 1 {
}
```
这一次运行`should_panic`测试,它会失败:
这一次运行 `should_panic` 测试,它会失败:
```
```text
running 1 test
test tests::greater_than_100 ... FAILED
@ -582,6 +579,6 @@ failures:
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
```
错误信息表明测试确实如期望 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 信息`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` 的不同选项。

View File

@ -2,37 +2,33 @@
> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-02-running-tests.md)
> <br>
> commit 55b294f20fc846a13a9be623bf322d8b364cee77
> commit db08b34db5f1c78b4866b391c802344ec94ecc38
就像`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` 则会告诉你位于分隔符 `--` 之后的相关参数。
### 并行或连续的运行测试
<!-- Are we safe assuming the reader will know enough about threads in this
context? -->
<!-- Yes /Carol -->
当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或依赖任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境。
当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境
例如,每一个测试都运行一些代码在硬盘上创建一个 `test-output.txt` 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。
例如,每一个测试都运行一些代码在硬盘上创建一个`test-output.txt`文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。
如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递 `--test-threads` 参数和希望使用线程的数量给测试二进制文件。例如:
如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递`--test-threads`参数和希望使用线程的数量给测试二进制文件。例如:
```
```text
$ cargo test -- --test-threads=1
```
这里将测试线程设置为 1告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间不过测试就不会在存在共享状态时潜在的相互干涉了。
### 显示测试输出
### 显示函数输出
如果测试通过了Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用`println!`而测试通过了,我们将不会在终端看到`println!`的输出:只会看到说明测试通过的行。如果测试失败了,就会看到任何标准输出和其他错误信息。
如果测试通过了Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用 `println!` 而测试通过了,我们将不会在终端看到 `println!` 的输出:只会看到说明测试通过的行。如果测试失败了,就会看到所有标准输出和其他错误信息。
例如,列表 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试
<span class="filename">Filename: src/lib.rs</span>
<span class="filename">文件名: src/lib.rs</span>
```rust
fn prints_and_returns_10(a: i32) -> i32 {
@ -58,12 +54,11 @@ mod tests {
}
```
<span class="caption">Listing 11-10: Tests for a function that calls `println!`
</span>
<span class="caption">列表 11-10一个调用了 `println!` 的函数的测试</span>
运行`cargo test`将会看到这些测试的输出:
运行 `cargo test` 将会看到这些测试的输出:
```
```text
running 2 tests
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED
@ -82,17 +77,17 @@ failures:
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
```
注意输出中哪里也不会出现`I got the value 4`,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,`I got the value 8`,则出现在输出的测试总结部分,也显示了测试失败的原因。
注意输出中哪里也不会出现 `I got the value 4`,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,`I got the value 8`,则出现在输出的测试总结部分,同时也显示了测试失败的原因。
如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过`--nocapture`参数来禁用:
如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过 `--nocapture` 参数来禁用:
```
```text
$ cargo test -- --nocapture
```
使用`--nocapture`参数再次运行列表 11-10 中的测试会显示:
使用 `--nocapture` 参数再次运行列表 11-10 中的测试会显示:
```
```text
running 2 tests
I got the value 4
I got the value 8
@ -114,11 +109,11 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
### 通过名称来运行测试的子集
有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。可以向`cargo test`传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。
有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这些代码相关的测试。可以向 `cargo test` 传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。
为了展示如何运行测试的子集,列表 11-11 使用`add_two`函数创建了三个测试来供我们选择运行哪一个:
为了展示如何运行测试的子集,列表 11-11 使用 `add_two` 函数创建了三个测试来供我们选择运行哪一个:
<span class="filename">Filename: src/lib.rs</span>
<span class="filename">文件名: src/lib.rs</span>
```rust
pub fn add_two(a: i32) -> i32 {
@ -146,11 +141,11 @@ mod tests {
}
```
<span class="caption">Listing 11-11: Three tests with a variety of names</span>
<span class="caption">列表 11-11不同名称的三个测试</span>
如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:
```
```text
running 3 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok
@ -161,11 +156,11 @@ test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
#### 运行单个测试
可以向`cargo test`传递任意测试的名称来只运行这个测试:
可以向 `cargo test` 传递任意测试的名称来只运行这个测试:
```
```text
$ cargo test one_hundred
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-06a75b4a1f2515e9
running 1 test
@ -174,15 +169,15 @@ test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
```
不能像这样指定多个测试名称,只有传递给`cargo test`的第一个值才会被使用。
不能像这样指定多个测试名称,只有传递给 `cargo test` 的第一个值才会被使用。
#### 过滤运行多个测试
然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含`add`,可以通过`cargo test add`来运行这两个测试:
然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含 `add`,可以通过 `cargo test add` 来运行这两个测试:
```
```text
$ cargo test add
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-06a75b4a1f2515e9
running 2 tests
@ -192,21 +187,13 @@ test tests::add_three_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
```
这运行了所有名字中带有`add`的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。
<!-- in what kind of situation might you need to run only some tests, when you
have lots and lots in a program? -->
<!-- We covered this in the first paragraph of the "Running a Subset of Tests
by Name" section, do you think it should be repeated so soon? Most people who
use tests have sufficient motivation for wanting to run a subset of the tests,
they just need to know how to do it with Rust, so we don't think this is a
point that needs to be emphasized multiple times. /Carol -->
这运行了所有名字中带有 `add` 的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。
### 除非指定否则忽略某些测试
有时一些特定的测试执行起来是非常耗费时间的,所以在运行大多数`cargo test`的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用`ignore`属性来标记耗时的测试来排除他们:
有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 `cargo test` 的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用 `ignore` 属性来标记耗时的测试来排除他们:
<span class="filename">Filename: src/lib.rs</span>
<span class="filename">文件名: src/lib.rs</span>
```rust
#[test]
@ -221,12 +208,12 @@ fn expensive_test() {
}
```
我们对想要排除的测试的`#[test]`之后增加了`#[ignore]`行。现在如果运行测试,就会发现`it_works`运行了,而`expensive_test`没有运行:
对想要排除的测试的 `#[test]` 之后增加了 `#[ignore]` 行。现在如果运行测试,就会发现 `it_works` 运行了,而 `expensive_test` 没有运行:
```
```text
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
Finished dev [unoptimized + debuginfo] target(s) in 0.24 secs
Running target/debug/deps/adder-ce99bcc2479f4607
running 2 tests
@ -242,22 +229,11 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
```
`expensive_test`被列为`ignored`,如果只希望运行被忽略的测试,可以使用`cargo test -- --ignored`来请求运行他们:
<!-- what does the double `-- --` mean? That seems interesting -->
<!-- We covered that in the second paragraph after the "Controlling How Tests
are Run" heading, and this section is beneath that heading, so I don't think a
back reference is needed /Carol -->
<!-- is that right, this way the program knows to run only the test with
`ignore` if we add this, or it knows to run all tests? -->
<!-- Is this unclear from the output that shows `expensive_test` was run and
the `it_works` test does not appear? I'm not sure how to make this clearer.
/Carol -->
`expensive_test` 被列为 `ignored`,如果只希望运行被忽略的测试,可以使用 `cargo test -- --ignored` 来请求运行他们:
```text
$ cargo test -- --ignored
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-ce99bcc2479f4607
running 1 test
@ -266,5 +242,4 @@ test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
```
通过控制运行哪些测试,可以确保运行`cargo test`的结果是快速的。当某个时刻需要检查`ignored`测试的结果而且你也有时间等待这个结果的话,可以选择执行`cargo test -- --ignored`。
通过控制运行哪些测试,可以确保运行 `cargo test` 的结果是快速的。当某个时刻需要检查 `ignored` 测试的结果而且你也有时间等待这个结果的话,可以选择执行 `cargo test -- --ignored`

View File

@ -2,23 +2,23 @@
> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-03-test-organization.md)
> <br>
> commit 55b294f20fc846a13a9be623bf322d8b364cee77
> commit 0665bc5646339a6bcda21838f46d4357b9435e75
正如之前提到的测试是一个很广泛的学科而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试***unit tests*)与**集成测试***integration tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。
正如之前提到的测试是一个很广泛的学科而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试***unit tests*)与 **集成测试***integration tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。
这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
编写这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
### 单元测试
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 *src* 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的`tests`模块,并使用`cfg(test)`标注模块。
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位代码位于何处和是否符合预期。单元测试位于 *src* 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的 `tests` 模块,并使用 `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 为我们生成了如下代码:
<span class="filename">Filename: src/lib.rs</span>
<span class="filename">文件名: src/lib.rs</span>
```rust
#[cfg(test)]
@ -29,13 +29,13 @@ mod tests {
}
```
这里自动生成了测试模块。`cfg`属性代表 *configuration* ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是`test`Rust 所提供的用于编译和运行测试的配置。通过使用这个属性Cargo 只会在我们主动使用`cargo test`运行测试时才编译测试代码。除了标注为`#[test]`的函数之外,还包括测试模块中可能存在的帮助函数。
这里自动生成了测试模块。`cfg` 属性代表 *configuration* ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是 `test`Rust 所提供的用于编译和运行测试的配置。通过使用这个属性Cargo 只会在我们主动使用 `cargo test` 运行测试时才编译测试代码。除了标注为 `#[test]` 的函数之外,还包括测试模块中可能存在的帮助函数。
#### 测试私有函数
测试社区中一直存在关于是否应该对私有函数进行单元测试的论战而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑列表 11-12 中带有私有函数`internal_adder`的代码:
测试社区中一直存在关于是否应该对私有函数进行单元测试的论战而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑列表 11-12 中带有私有函数 `internal_adder` 的代码:
<span class="filename">Filename: src/lib.rs</span>
<span class="filename">文件名: src/lib.rs</span>
```rust
pub fn add_two(a: i32) -> i32 {
@ -57,29 +57,21 @@ mod tests {
}
```
<span class="caption">Listing 11-12: Testing a private function</span>
<span class="caption">列表 11-12测试私有函数</span>
<!-- I'm not clear on why we would assume this might not be fine, why are we
highlighting this specifically? -->
<!-- We're addressing experience that the reader might bring with them from
other languages where this is not allowed; I added a sentence mentioning "other
languages" at the beginning of this section. Also testing private functions
from integration tests is not allowed, so if you did want to do this, you'd
have to do it in unit tests. /Carol -->
注意`internal_adder`函数并没有标记为`pub`,不过因为测试也不过是 Rust 代码而`tests`也仅仅是另一个模块,我们完全可以在测试中导入和调用`internal_adder`。如果你并不认为私有函数应该被测试Rust 也不会强迫你这么做。
注意 `internal_adder` 函数并没有标记为 `pub`,不过因为测试也不过是 Rust 代码同时 `tests` 也仅仅是另一个模块,我们完全可以在测试中导入和调用 `internal_adder`。如果你并不认为私有函数应该被测试Rust 也不会强迫你这么做。
### 集成测试
在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 *tests* 目录。
在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 *tests* 目录。
#### *tests* 目录
为了编写集成测试,需要在项目根目录创建一个 *tests* 目录,与 *src* 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个文件夹中创建任意多的测试文件Cargo 会将每一个文件当作单独的 crate 来编译。
为了编写集成测试,需要在项目根目录创建一个 *tests* 目录,与 *src* 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件Cargo 会将每一个文件当作单独的 crate 来编译。
让我们试一试吧!保留列表 11-12 中 *src/lib.rs* 的代码。创建一个 *tests* 目录,新建一个文件 *tests/integration_test.rs*,并输入列表 11-13 中的代码。
<span class="filename">Filename: tests/integration_test.rs</span>
<span class="filename">文件名: tests/integration_test.rs</span>
```rust,ignore
extern crate adder;
@ -90,17 +82,16 @@ fn it_adds_two() {
}
```
<span class="caption">Listing 11-13: An integration test of a function in the
`adder` crate </span>
<span class="caption">列表 11-13一个 `adder` crate 中函数的集成测试</span>
我们在顶部增加了`extern crate adder`,这在单元测试中是不需要的。这是因为每一个`tests`目录中的测试文件都是完全独立的 crate所以需要在每一个文件中导入库。集成测试就像其他使用者那样通过导入 crate 并只使用公有 API 来使用库文件
我们在顶部增加了 `extern crate adder`,这在单元测试中是不需要的。这是因为每一个 `tests` 目录中的测试文件都是完全独立的 crate所以需要在每一个文件中导入库。集成测试就像其他使用者那样通过导入 crate 并只使用公有 API。
并不需要将 *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
Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 0.31 secs
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running target/debug/deps/adder-abcabcabc
running 1 test
@ -122,22 +113,17 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
```
<!-- what are the doc tests? How do we tell the difference between unit and
integration tests here? -->
<!-- We mentioned documentation tests in the beginning of this chapter /Carol
-->
现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 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* 目录中增加更多文件,这里就会有更多集成测试部分。
我们仍然可以通过指定测试函数的名称作为`cargo test`的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用`cargo test`的`--test`后跟文件的名称:
我们仍然可以通过指定测试函数的名称作为 `cargo test` 的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用 `cargo test` `--test` 后跟文件的名称:
```
```text
$ cargo test --test integration_test
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/integration_test-952a27e0126bb565
running 1 test
@ -152,11 +138,11 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
随着集成测试的增加,你可能希望在 `tests` 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 *tests* 目录中的文件都被编译为单独的 crate。
将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到第七章学习的如何将代码分隔进模块和文件那样*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为。
将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到第七章学习的如何将代码分隔进模块和文件的知识*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为
对于 *tests* 目录中文件的不同行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 *tests/common.rs* 并将`setup`函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码:
对于 *tests* 目录中不同文件的行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 *tests/common.rs* 并将 `setup` 函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码:
<span class="filename">Filename: tests/common.rs</span>
<span class="filename">文件名: tests/common.rs</span>
```rust
pub fn setup() {
@ -164,9 +150,9 @@ pub fn setup() {
}
```
如果再次运行测试,将会在测试结果中看到一个对应 *common.rs* 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了`setup`函数:
如果再次运行测试,将会在测试结果中看到一个对应 *common.rs* 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了 `setup` 函数:
```
```text
running 1 test
test tests::internal ... ok
@ -192,17 +178,13 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
```
<!-- The new section is lines 6-10, will ghost everything else in libreoffice
/Carol -->
`common` 出现在测试结果中并显示 `running 0 tests`,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。
为了使 `common` 不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建 *tests/common.rs*,而是创建 *tests/common/mod.rs*。当将 `setup` 代码移动到 *tests/common/mod.rs* 并去掉 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。
`common`出现在测试结果中并显示`running 0 tests`,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。
一旦拥有了 *tests/common/mod.rs*,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 *tests/integration_test.rs* 中调用 `setup` 函数的 `it_adds_two` 测试的例子:
为了使`common`不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建*tests/common.rs*,而是创建 *tests/common/mod.rs*。当将`setup`代码移动到 *tests/common/mod.rs* 并去掉 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。
一旦拥有了 *tests/common/mod.rs*,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 *tests/integration_test.rs* 中调用`setup`函数的`it_adds_two`测试的例子:
<span class="filename">Filename: tests/integration_test.rs</span>
<span class="filename">文件名: tests/integration_test.rs</span>
```rust,ignore
extern crate adder;
@ -216,16 +198,16 @@ fn it_adds_two() {
}
```
注意`mod common;`声明与第七章中的模块声明相同。接着在测试函数中就可以调用`common::setup()`了。
注意 `mod common;` 声明与第七章中的模块声明相同。接着在测试函数中就可以调用 `common::setup()` 了。
#### 二进制 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 仍然是很重要的。
接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!

View File

@ -1,35 +1,16 @@
# 一个 I/O 项目:构建一个小巧的 grep
# 一个 I/O 项目:构建一个命令行程序
> [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 1f432fc231cfbc310433ab2a354d77058444288c
<!-- We might need a more descriptive title, something that captures the new
elements we're introducing -- are we going to cover things like environment
variables more in later chapters, or is this the only place we explain how to
use them? -->
<!-- This is the only place we were planning on explaining both environment
variables and printing to standard error. These are things that people commonly
want to know how to do in Rust, but there's not much more than what we've said
here about them, people just want to know how to do them in Rust. We realize
that those sections make this chapter long, but think it's worth it to include
information that people want. We've gotten really positive feedback from people
who have read this chapter online; people who like learning through projects
have really enjoyed this chapter. /Carol-->
> commit 096db3d06b25692ee701750e1f995ba00d0f5c55
本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。
Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是“Globally search a Regular Expression and Print.”的首字母缩写。`grep`最简单的使用场景是使用如下步骤在特定文件中搜索指定字符串:
Rust 的运行速度、安全性、**单二进制文件** 输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件名和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。
- 获取一个文件和一个字符串作为参数。
- 读取文件
- 寻找文件中包含字符串参数的行
- 打印出这些行
在这个过程中,我们会展示如何让我们的命令行工具利用很多命令行工具中用到的终端功能。读取环境变量来使得用户可以配置工具的行为。打印到标准错误控制流(`stderr` 而不是标准输出(`stdout`),例如这样用户可以选择将成功输出重定向到文件中而仍然在屏幕上显示错误信息。
我们还会展示如何使用环境变量和打印到标准错误而不是标准输出;这些功能在命令行工具中是很常用的。
一位 Rust 社区的成员Andrew Gallant已经创建了一个功能完整且非常快速的`grep`版本,叫做`ripgrep`。相比之下,我们的`grep`将非常简单,本章将交给你一些帮助你理解像`ripgrep`这样真实项目的背景知识。
一位 Rust 社区的成员Andrew Gallant已经创建了一个功能完整且非常快速的 `grep` 版本,叫做 [`ripgrep`](https://github.com/BurntSushi/ripgrep)<!-- ignore -->。相比之下,我们的 `grep` 版本将非常简单,本章将教会你一些帮助理解像 `ripgrep` 这样真实项目的背景知识。
这个项目将会结合之前所学的一些内容:
@ -39,12 +20,4 @@ Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支
- 合理的使用 trait 和生命周期(第十章)
- 测试(第十一章)
另外,我还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第十三章和第十七章中详细介绍。
让我们一如既往的使用`cargo new`创建一个新项目。我们称之为`greprs`以便与可能已经安装在系统上的`grep`工具相区别:
```
$ cargo new --bin greprs
Created binary (application) `greprs` project
$ cd greprs
```
另外还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第十三章和第十七章中详细介绍。

View File

@ -2,24 +2,23 @@
> [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 b8e4fcbf289b82c12121b282747ce05180afb1fb
> commit 50658e654fb6a9208b635179cdd79939aa0ab133
第一个任务是让`greprs`能够接受两个命令行参数:文件名和要搜索的字符串。也就是说希望能够使用`cargo run`,要搜索的字符串和被搜索的文件的路径来运行程序,像这样
一如之前使用 `cargo new` 新建一个项目。我们称之为 `minigrep` 以便与可能已经安装在系统上的`grep`工具相区别
```text
$ cargo new --bin minigrep
Created binary (application) `minigrep` project
$ cd minigrep
```
第一个任务是让 `minigrep` 能够接受两个命令行参数:文件名和要搜索的字符串。也就是说我们希望能够使用 `cargo run`、要搜索的字符串和被搜索的文件的路径来运行程序,像这样:
```text
$ cargo run searchstring example-filename.txt
```
现在`cargo new`生成的程序忽略任何传递给它的参数。crates.io 上有一些现存的可以帮助我们接受命令行参数的库,不过因为我们正在学习,让我们实现一个。
<!--Below -- I'm not clear what we need the args function for, yet, can you set
it out more concretely? Otherwise, will it make more sense in context of the
code later? Is this function needed to allow our function to accept arguments,
is that was "args" is for? -->
<!-- We mentioned in the intro to this chapter that grep takes as arguments a
filename and a string. I've added an example of how we want to run our
resulting tool and what we want the behavior to be, please let me know if this
doesn't clear it up. /Carol-->
现在 `cargo new` 生成的程序忽略任何传递给它的参数。crates.io 上有一些现成的库可以帮助我们接受命令行参数,不过因为正在学习,让我们自己来实现一个。
### 读取参数值