check to ch11-03

This commit is contained in:
KaiserY 2019-12-02 10:13:32 +08:00
parent b53f198f6c
commit 8b6f370fae
5 changed files with 68 additions and 58 deletions

View File

@ -2,9 +2,9 @@
> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-03-lifetime-syntax.md) > [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-03-lifetime-syntax.md)
> <br> > <br>
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f > commit 426f3e4ec17e539ae9905ba559411169d303a031
当在第四章讨论 “引用和借用” 部分时我们遗漏了一个重要的细节Rust 中的每一个引用都有其 **生命周期***lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 当在第四章讨论 [“引用和借用”][references-and-borrowing] 部分时我们遗漏了一个重要的细节Rust 中的每一个引用都有其 **生命周期***lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
生命周期的概念某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。查看第十九章 “高级生命周期” 部分了解更多的细节。 生命周期的概念某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。查看第十九章 “高级生命周期” 部分了解更多的细节。
@ -104,7 +104,9 @@ fn main() {
<span class="caption">示例 10-20`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个</span> <span class="caption">示例 10-20`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个</span>
参考之前第四章中的 “字符串 slice 作为参数” 部分中更多关于为什么上面例子中的参数正符合我们期望的讨论。 注意这个函数获取作为引用的字符串 slice因为我们不希望 `longest` 函数获取参数的所有权。我们期望该函数接受 `String` 的 slice参数 `string1` 的类型)和字符串字面值(包含于参数 `string2`
参考之前第四章中的 [“字符串 slice 作为参数”][string-slices-as-parameters] 部分中更多关于为什么示例 10-20 的参数正符合我们期望的讨论。
如果尝试像示例 10-21 中那样实现 `longest` 函数,它并不能编译: 如果尝试像示例 10-21 中那样实现 `longest` 函数,它并不能编译:
@ -363,15 +365,13 @@ fn first_word<'a>(s: &'a str) -> &'a str {
函数或方法的参数的生命周期被称为 **输入生命周期***input lifetimes*),而返回值的生命周期被称为 **输出生命周期***output lifetimes*)。 函数或方法的参数的生命周期被称为 **输入生命周期***input lifetimes*),而返回值的生命周期被称为 **输出生命周期***output lifetimes*)。
编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。 编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 `fn` 定义,以及 `impl` 块。
这些规则适用于 `fn` 定义,以及 `impl` 块。
第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。 第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。
第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。 第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。
第三条规则是如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self``&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。使得方法更容易读写,因为只需更少的符号。 第三条规则是如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self``&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。第三条规则使得方法更容易读写,因为只需更少的符号。
假设我们自己就是编译器。并应用这些规则来计算示例 10-26 中 `first_word` 函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期: 假设我们自己就是编译器。并应用这些规则来计算示例 10-26 中 `first_word` 函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:
@ -452,7 +452,7 @@ impl<'a> ImportantExcerpt<'a> {
### 静态生命周期 ### 静态生命周期
这里有一种特殊的生命周期值得讨论:`'static`,其生命周期存活于整个程序期间。所有的字符串字面值都拥有 `'static` 生命周期,我们也可以选择像下面这样标注出来: 这里有一种特殊的生命周期值得讨论:`'static`,其生命周期**能够**存活于整个程序期间。所有的字符串字面值都拥有 `'static` 生命周期,我们也可以选择像下面这样标注出来:
```rust ```rust
let s: &'static str = "I have a static lifetime."; let s: &'static str = "I have a static lifetime.";
@ -488,3 +488,8 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a st
这一章介绍了很多的内容现在你知道了泛型类型参数、trait 和 trait bounds 以及泛型生命周期类型你已经准备好编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率! 这一章介绍了很多的内容现在你知道了泛型类型参数、trait 和 trait bounds 以及泛型生命周期类型你已经准备好编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率!
你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作! 你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!
[references-and-borrowing]:
ch04-02-references-and-borrowing.html#references-and-borrowing
[string-slices-as-parameters]:
ch04-03-slices.html#string-slices-as-parameters

View File

@ -4,14 +4,6 @@
> <br> > <br>
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f > commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
> 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 存在的有效方法,而证明其不存在时则显得令人绝望的不足。
>
> Edsger W. Dijkstra【谦卑的程序员】1972
Edsger W. Dijkstra 在其 1972 年的文章【谦卑的程序员】“The Humble Programmer”中说到 “软件测试是证明 bug 存在的有效方法而证明其不存在时则显得令人绝望的不足。”“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 在其 1972 年的文章【谦卑的程序员】“The Humble Programmer”中说到 “软件测试是证明 bug 存在的有效方法而证明其不存在时则显得令人绝望的不足。”“Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.”)这并不意味着我们不该尽可能地测试软件!
程序的正确性意味着代码如我们期望的那样运行。Rust 是一个相当注重正确性的编程语言不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫不过它不可能捕获所有种类的错误。为此Rust 也在语言本身包含了编写软件测试的支持。 程序的正确性意味着代码如我们期望的那样运行。Rust 是一个相当注重正确性的编程语言不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫不过它不可能捕获所有种类的错误。为此Rust 也在语言本身包含了编写软件测试的支持。

View File

@ -2,7 +2,7 @@
> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-01-writing-tests.md) > [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-01-writing-tests.md)
> <br> > <br>
> commit 820ac357f6cf0e866e5a8e7a9c57dd3e17e9f8ca > commit cc6a1ef2614aa94003566027b285b249ccf961fa
Rust 中的测试函数是用来验证非测试代码是否按照期望的方式运行的。测试函数体通常执行如下三种操作: Rust 中的测试函数是用来验证非测试代码是否按照期望的方式运行的。测试函数体通常执行如下三种操作:
@ -10,7 +10,6 @@ Rust 中的测试函数是用来验证非测试代码是否按照期望的方式
2. 运行需要测试的代码 2. 运行需要测试的代码
3. 断言其结果是我们所期望的 3. 断言其结果是我们所期望的
让我们看看 Rust 提供的专门用来编写测试的功能:`test` 属性、一些宏和 `should_panic` 属性。 让我们看看 Rust 提供的专门用来编写测试的功能:`test` 属性、一些宏和 `should_panic` 属性。
### 测试函数剖析 ### 测试函数剖析
@ -29,7 +28,6 @@ $ cargo new adder --lib
$ cd adder $ cd adder
``` ```
adder 库中 `src/lib.rs` 的内容应该看起来如示例 11-1 所示: adder 库中 `src/lib.rs` 的内容应该看起来如示例 11-1 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -75,13 +73,13 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
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`。在下一部分 [“控制测试如何运行”][controlling-how-tests-are-run] 会讨论忽略和过滤测试。
`0 measured` 统计是针对性能测试的。性能测试benchmark tests在编写本书时仍只能用于 Rust 开发版nightly Rust。请查看 [性能测试的文档][bench] 了解更多。 `0 measured` 统计是针对性能测试的。性能测试benchmark tests在编写本书时仍只能用于 Rust 开发版nightly Rust。请查看 [性能测试的文档][bench] 了解更多。
[bench]: https://doc.rust-lang.org/unstable-book/library-features/test.html [bench]: https://doc.rust-lang.org/unstable-book/library-features/test.html
测试输出的中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。 测试输出的中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 [“文档注释作为测试][doc-comments] 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。
让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样: 让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样:
@ -139,7 +137,7 @@ test tests::another ... FAILED
failures: failures:
---- tests::another stdout ---- ---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:8 thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:9
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures: failures:
@ -152,7 +150,7 @@ error: test failed
<span class="caption">示例 11-4一个测试通过和一个测试失败的测试结果</span> <span class="caption">示例 11-4一个测试通过和一个测试失败的测试结果</span>
`test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和摘要之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为在*src/lib.rs* 的第 10 行 `panicked at 'Make this test fail'` 而失败。下一部分列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。我们可以通过使用失败测试的名称来只运行这个测试,以便调试;下一部分 “控制测试如何运行” 会讲到更多运行测试的方法。 `test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和摘要之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为在*src/lib.rs* 的第 10 行 `panicked at 'Make this test fail'` 而失败。下一部分列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。我们可以通过使用失败测试的名称来只运行这个测试,以便调试;下一部分 [“控制测试如何运行”][controlling-how-tests-are-run] 会讲到更多运行测试的方法。
最后是摘要行:总体上讲,测试结果是 `FAILED`。有一个测试通过和一个测试失败。 最后是摘要行:总体上讲,测试结果是 `FAILED`。有一个测试通过和一个测试失败。
@ -169,14 +167,14 @@ error: test failed
```rust ```rust
# fn main() {} # fn main() {}
#[derive(Debug)] #[derive(Debug)]
pub struct Rectangle { struct Rectangle {
length: u32,
width: u32, width: u32,
height: u32,
} }
impl Rectangle { impl Rectangle {
pub fn can_hold(&self, other: &Rectangle) -> bool { fn can_hold(&self, other: &Rectangle) -> bool {
self.length > other.length && self.width > other.width self.width > other.width && self.height > other.height
} }
} }
``` ```
@ -195,8 +193,8 @@ mod tests {
#[test] #[test]
fn larger_can_hold_smaller() { fn larger_can_hold_smaller() {
let larger = Rectangle { length: 8, width: 7 }; let larger = Rectangle { width: 8, height: 7 };
let smaller = Rectangle { length: 5, width: 1 }; let smaller = Rectangle { width: 5, height: 1 };
assert!(larger.can_hold(&smaller)); assert!(larger.can_hold(&smaller));
} }
@ -205,7 +203,7 @@ mod tests {
<span class="caption">示例 11-6一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形</span> <span class="caption">示例 11-6一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形</span>
注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 “私有性规则” 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用 glob 全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。 注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 [“路径用于引用模块树中的项”][paths-for-referring-to-an-item-in-the-module-tree] 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用 glob 全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。
我们将测试命名为 `larger_can_hold_smaller`,并创建所需的两个 `Rectangle` 实例。接着调用 `assert!` 宏并传递 `larger.can_hold(&smaller)` 调用的结果作为参数。这个表达式预期会返回 `true`,所以测试应该通过。让我们拭目以待! 我们将测试命名为 `larger_can_hold_smaller`,并创建所需的两个 `Rectangle` 实例。接着调用 `assert!` 宏并传递 `larger.can_hold(&smaller)` 调用的结果作为参数。这个表达式预期会返回 `true`,所以测试应该通过。让我们拭目以待!
@ -233,8 +231,8 @@ mod tests {
#[test] #[test]
fn smaller_cannot_hold_larger() { fn smaller_cannot_hold_larger() {
let larger = Rectangle { length: 8, width: 7 }; let larger = Rectangle { width: 8, height: 7 };
let smaller = Rectangle { length: 5, width: 1 }; let smaller = Rectangle { width: 5, height: 1 };
assert!(!smaller.can_hold(&larger)); assert!(!smaller.can_hold(&larger));
} }
@ -256,15 +254,15 @@ test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```rust,not_desired_behavior ```rust,not_desired_behavior
# fn main() {} # fn main() {}
# #[derive(Debug)] # #[derive(Debug)]
# pub struct Rectangle { # struct Rectangle {
# length: u32,
# width: u32, # width: u32,
# height: u32,
# } # }
// --snip-- // --snip--
impl Rectangle { impl Rectangle {
pub fn can_hold(&self, other: &Rectangle) -> bool { fn can_hold(&self, other: &Rectangle) -> bool {
self.length < other.length && self.width > other.width self.width < other.width && self.height > other.height
} }
} }
``` ```
@ -279,8 +277,8 @@ test tests::larger_can_hold_smaller ... FAILED
failures: failures:
---- tests::larger_can_hold_smaller stdout ---- ---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed: thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed:
larger.can_hold(&smaller)', src/lib.rs:22:8 larger.can_hold(&smaller)', src/lib.rs:22:9
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures: failures:
@ -316,7 +314,7 @@ mod tests {
} }
``` ```
<span class="caption">示例 11-7使用 `assert_eq!` 宏测试 `add_two`</span> <span class="caption">示例 11-7使用 `assert_eq!` 宏测试 `add_two` 函数</span>
测试通过了! 测试通过了!
@ -347,9 +345,9 @@ test tests::it_adds_two ... FAILED
failures: failures:
---- tests::it_adds_two stdout ---- ---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)` thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)`
left: `4`, left: `4`,
right: `5`', src/lib.rs:11:8 right: `5`', src/lib.rs:11:9
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures: failures:
@ -364,11 +362,11 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
`assert_ne!` 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 **会** 是什么,不过能确定值绝对 **不会** 是什么的时候,这个宏最有用处。例如,如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。 `assert_ne!` 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 **会** 是什么,不过能确定值绝对 **不会** 是什么的时候,这个宏最有用处。例如,如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。
`assert_eq!``assert_ne!` 宏在底层分别使用了 `==``!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq``Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C “可派生 trait” 中有更多关于这些和其他派生 trait 的详细信息。 `assert_eq!``assert_ne!` 宏在底层分别使用了 `==``!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq``Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C [“可派生 trait”][derivable-traits] 中有更多关于这些和其他派生 trait 的详细信息。
### 自定义失败信息 ### 自定义失败信息
你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!``assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第八章的“使用 `+` 运算符或 `format!` 宏拼接字符串”部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。 你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!``assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第八章的 [“使用 `+` 运算符或 `format!` 宏拼接字符串”][concatenation-with-the--operator-or-the-format-macro] 部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。
例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中: 例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
@ -412,8 +410,8 @@ test tests::greeting_contains_name ... FAILED
failures: failures:
---- tests::greeting_contains_name stdout ---- ---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'assertion failed: thread 'tests::greeting_contains_name' panicked at 'assertion failed:
result.contains("Carol")', src/lib.rs:12:8 result.contains("Carol")', src/lib.rs:12:9
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures: failures:
@ -437,8 +435,8 @@ fn greeting_contains_name() {
```text ```text
---- tests::greeting_contains_name stdout ---- ---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'Greeting did not thread 'tests::greeting_contains_name' panicked at 'Greeting did not
contain name, value was `Hello!`', src/lib.rs:12:8 contain name, value was `Hello!`', src/lib.rs:12:9
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
``` ```
@ -532,7 +530,7 @@ 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
``` ```
这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了 `#[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 信息:
@ -597,8 +595,8 @@ test tests::greater_than_100 ... FAILED
failures: failures:
---- tests::greater_than_100 stdout ---- ---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at 'Guess value must be thread 'tests::greater_than_100' panicked at 'Guess value must be
greater than or equal to 1, got 200.', src/lib.rs:11:12 greater than or equal to 1, got 200.', src/lib.rs:11:13
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
note: Panic did not include expected string 'Guess value must be less than or note: Panic did not include expected string 'Guess value must be less than or
equal to 100' equal to 100'
@ -629,6 +627,18 @@ mod tests {
} }
``` ```
这里我们将 `it_works` 改为返回 Result。同时在函数体中在成功时返回 `Ok(())` 而不是 `assert_eq!`,而失败时返回带有 `String``Err`。跟之前一样,这个测试可能成功或失败,不过不再通过 panic可以通过 `Result<T, E>` 来判断结果。为此不能在对这些函数使用 `#[should_panic]`;而是应该返回 `Err` 现在 `it_works` 函数的返回值类型为 `Result<(), String>`。在函数体中,不同于调用 `assert_eq!` 宏,而是在测试通过时返回 `Ok(())`,在测试失败时返回带有 `String``Err`
这样编写测试来返回 `Result<T, E>` 就可以在函数体中使用问号运算符,如此可以方便的编写任何运算符会返回 `Err` 成员的测试。
不能对这些使用 `Result<T, E>` 的测试使用 `#[should_panic]` 注解。相反应该在测试失败时直接返回 `Err` 值。
现在你知道了几种编写测试的方法,让我们看看运行测试时会发生什么,和可以用于 `cargo test` 的不同选项。 现在你知道了几种编写测试的方法,让我们看看运行测试时会发生什么,和可以用于 `cargo test` 的不同选项。
[concatenation-with-the--operator-or-the-format-macro]:
ch08-02-strings.html#concatenation-with-the--operator-or-the-format-macro
[controlling-how-tests-are-run]:
ch11-02-running-tests.html#controlling-how-tests-are-run
[derivable-traits]: appendix-03-derivable-traits.html
[doc-comments]: ch14-02-publishing-to-crates-io.html#documentation-comments-as-tests
[paths-for-referring-to-an-item-in-the-module-tree]: ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html

View File

@ -2,7 +2,7 @@
> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-02-running-tests.md) > [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-02-running-tests.md)
> <br> > <br>
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f > commit 42b802f26197f9a066e4a671d2b062af25972c13
就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。可以指定命令行参数来改变 `cargo test` 的默认行为。例如,`cargo test` 生成的二进制文件的默认行为是并行的运行所有测试,并截获测试运行过程中产生的输出,阻止他们被显示出来,使得阅读测试结果相关的内容变得更容易。 就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。可以指定命令行参数来改变 `cargo test` 的默认行为。例如,`cargo test` 生成的二进制文件的默认行为是并行的运行所有测试,并截获测试运行过程中产生的输出,阻止他们被显示出来,使得阅读测试结果相关的内容变得更容易。
@ -66,10 +66,10 @@ test tests::this_test_will_fail ... FAILED
failures: failures:
---- tests::this_test_will_fail stdout ---- ---- tests::this_test_will_fail stdout ----
I got the value 8 I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
left: `5`, left: `5`,
right: `10`', src/lib.rs:19:8 right: `10`', src/lib.rs:19:9
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures: failures:
@ -95,7 +95,7 @@ I got the value 8
test tests::this_test_will_pass ... ok test tests::this_test_will_pass ... ok
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
left: `5`, left: `5`,
right: `10`', src/lib.rs:19:8 right: `10`', src/lib.rs:19:9
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
test tests::this_test_will_fail ... FAILED test tests::this_test_will_fail ... FAILED
@ -208,7 +208,7 @@ fn it_works() {
#[test] #[test]
#[ignore] #[ignore]
fn expensive_test() { fn expensive_test() {
// code that takes an hour to run // 需要运行一个小时的代码
} }
``` ```

View File

@ -2,7 +2,7 @@
> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/src/ch11-03-test-organization.md) > [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/src/ch11-03-test-organization.md)
> <br> > <br>
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f > commit 4badf9a8574c12794795b05954baf5adc579fa90
本章一开始就提到测试是一个复杂的概念而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试***unit tests*)与 **集成测试***integration tests*)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。 本章一开始就提到测试是一个复杂的概念而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试***unit tests*)与 **集成测试***integration tests*)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。
@ -12,7 +12,7 @@
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的某个单元的代码功能是否符合预期。单元测试与他们要测试的代码共同存放在位于 *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)]` 来指定他们不应该被包含进编译结果中。
@ -214,3 +214,6 @@ fn it_adds_two() {
Rust 的测试功能提供了一个确保即使你改变了函数的实现方式,也能继续以期望的方式运行的途径。单元测试独立地验证库的不同部分,也能够测试私有函数实现细节。集成测试则检查多个部分是否能结合起来正确地工作,并像其他外部代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug不过测试对于减少代码中不符合期望行为的逻辑 bug 仍然是很重要的。 Rust 的测试功能提供了一个确保即使你改变了函数的实现方式,也能继续以期望的方式运行的途径。单元测试独立地验证库的不同部分,也能够测试私有函数实现细节。集成测试则检查多个部分是否能结合起来正确地工作,并像其他外部代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug不过测试对于减少代码中不符合期望行为的逻辑 bug 仍然是很重要的。
让我们将本章和其他之前章节所学的知识组合起来,在下一章一起编写一个项目! 让我们将本章和其他之前章节所学的知识组合起来,在下一章一起编写一个项目!
[separating-modules-into-files]:
ch07-05-separating-modules-into-different-files.html