diff --git a/docs/ch03-05-control-flow.html b/docs/ch03-05-control-flow.html index a9c724f..5ae5344 100644 --- a/docs/ch03-05-control-flow.html +++ b/docs/ch03-05-control-flow.html @@ -284,8 +284,7 @@ the value is: 50

所有数组中的五个元素都如期被打印出来。尽管index在某一时刻会到达值5,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。

不过这个过程是容易出错的;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。

可以使用for循环来对一个集合的每个元素执行一些代码,来作为一个更有效率替代。for循环看起来像这样:

-
-Filename: src/main.rs +

Filename: src/main.rs

fn main() {
     let a = [10, 20, 30, 40, 50];
 
@@ -294,10 +293,8 @@ the value is: 50
     }
 }
 
-
-

Listing 3-6: Looping through each element of a collection using a for loop

-
-
+

Listing 3-6: Looping through each element of a collection +using a for loop

当运行这段代码,将看到与列表 3-5 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 机会。

例如,在列表 3-5 的代码中,如果从数组a中移除一个元素但忘记更新条件为while index < 4,代码将会 panic。使用for循环的话,就不需要惦记着在更新数组元素数量时修改其他的代码了。

for循环的安全性和简洁性使得它在成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如列表 3-5 中使用while循环的倒计时例子,大部分 Rustacean 也会使用for循环。这么做的方式是使用Range,它是标准库提供的用来生成从一个数字开始到另一个数字结束的所有数字序列的类型。

diff --git a/docs/ch07-01-mod-and-the-filesystem.html b/docs/ch07-01-mod-and-the-filesystem.html index b738715..8179bc0 100644 --- a/docs/ch07-01-mod-and-the-filesystem.html +++ b/docs/ch07-01-mod-and-the-filesystem.html @@ -229,7 +229,7 @@ mod server { }

注意这个模块文件中我们也使用了一个mod声明;这是因为我们希望server成为network的一个子模块。

-

现在再次运行cargo build。成功!不过我们还需要再提取出另一个模块:server。因为这是一个子模块————也就是模块中的模块————目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;替换server模块的内容:

+

现在再次运行cargo build。成功!不过我们还需要再提取出另一个模块:server。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;替换server模块的内容:

Filename: src/network.rs

fn connect() {
 }
diff --git a/docs/ch11-01-writing-tests.html b/docs/ch11-01-writing-tests.html
index de8b0dc..11785ec 100644
--- a/docs/ch11-01-writing-tests.html
+++ b/docs/ch11-01-writing-tests.html
@@ -264,133 +264,285 @@ mod tests {
     }
 }
 
-

Cargo 编译并运行了测试。这里有两部分输出:本章我们将关注第一部分。第二部分是文档测试的输出,第十四章会介绍他们。现在注意看这一行:

-
test it_works ... ok
+

因为这里can_hold函数的正确结果是false,我们需要将这个结果取反后传递给assert!宏。这样的话,测试就会通过而can_hold将返回false

+
running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
 
-

it_works文本来源于测试函数的名称。

-

这里也有一行总结告诉我们所有测试的聚合结果:

-
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-

assert!

-

空的测试函数之所以能通过是因为任何没有panic!的测试都是通过的,而任何panic!的测试都算是失败。让我们使用`assert!宏来使测试失败:

-

Filename: src/lib.rs

-
#[test]
-fn it_works() {
-    assert!(false);
+

这个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将can_hold方法中比较长度时本应使用大于号的地方改成小于号:

+
#[derive(Debug)]
+pub struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+impl Rectangle {
+    pub fn can_hold(&self, other: &Rectangle) -> bool {
+        self.length < other.length && self.width > other.width
+    }
 }
 
-

assert!宏由标准库提供,它获取一个参数,如果参数是true,什么也不会发生。如果参数是false,这个宏会panic!。再次运行测试:

-
$ cargo test
-   Compiling adder v0.1.0 (file:///projects/adder)
-    Finished debug [unoptimized + debuginfo] target(s) in 0.22 secs
-     Running target/debug/deps/adder-abcabcabc
-
-running 1 test
-test it_works ... FAILED
+

现在运行测试会产生:

+
running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... FAILED
 
 failures:
 
----- it_works stdout ----
-    thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
+---- tests::larger_can_hold_smaller stdout ----
+    thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed:
+    larger.can_hold(&smaller)', src/lib.rs:22
 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
+
+

我们的测试捕获了 bug!因为larger.length是 8 而smaller.length 是 5,can_hold中的长度比较现在返回false因为 8 不小于 5。

+

使用assert_eq!assert_ne!宏来测试相等

+

测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向assert!宏传递一个使用==宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来方便处理这些操作:assert_eq!assert_ne!。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试为什么失败,而assert!只会打印出它从==表达式中得到了false值,而不是导致false值的原因。

+

列表 11-7 中,让我们编写一个对其参数加二并返回结果的函数add_two。接着使用assert_eq!宏测试这个函数:

+

Filename: src/lib.rs

+
pub fn add_two(a: i32) -> i32 {
+    a + 2
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn it_adds_two() {
+        assert_eq!(4, add_two(2));
+    }
+}
+
+

Listing 11-7: Testing the function add_two using the +assert_eq! macro

+

测试通过了!

+
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 ... okok表明测试通过了!

+

在代码中引入一个 bug 来看看使用assert_eq!的测试失败是什么样的。修改add_two函数的实现使其加 3:

+
pub fn add_two(a: i32) -> i32 {
+    a + 3
+}
+
+

再次运行测试:

+
running 1 test
+test tests::it_adds_two ... FAILED
 
 failures:
-    it_works
+
+---- tests::it_adds_two stdout ----
+    thread 'tests::it_adds_two' panicked at 'assertion failed: `(left ==
+    right)` (left: `4`, right: `5`)', src/lib.rs:11
+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
-
-error: test failed
 
-

Rust 表明测试失败了:

-
test it_works ... FAILED
-
-

并展示了测试是因为src/lib.rs的第 5 行assert!宏得到了一个false`值而失败的:

-
thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
-
-

失败的测试也体现在了总结行中:

-
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
-
-

使用assert_eq!assert_ne!宏来测试相等

-

测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向assert!宏传递一个使用==宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来编译处理这些操作:assert_eq!assert_ne!。这两个宏分别比较两个值是相等还是不相等。使用这些宏的另一个优势是当断言失败时他们会打印出这两个值具体是什么,以便于观察测试为什么失败,而assert!只会打印出它从==表达式中得到了false值。

-

下面是分别使用这两个宏其会测试通过的例子:

+

测试捕获到了 bug!it_adds_two测试失败并显示信息assertion failed: `(left == right)` (left: `4`, right: `5`)。这个信息有助于我们开始调试:它说assert_eq!left参数是 4,而right参数,也就是add_two(2)的结果,是 5。

+

注意在一些语言和测试框架中,断言两个值相等的函数的参数叫做expectedactual,而且指定参数的顺序是需要注意的。然而在 Rust 中,他们则叫做leftright,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成assert_eq!(add_two(2), 4),这时错误信息会变成assertion failed: `(left == right)` (left: `5`, right: `4`)

+

assert_ne!宏在传递给它的两个值不相等时通过而在相等时失败。这个宏在代码按照我们期望运行时不确定值应该是什么,不过知道他们绝对不应该是什么的时候最有用处。例如,如果一个函数确定会以某种方式改变其输出,不过这种方式由运行测试是星期几来决定,这时最好的断言可能就是函数的输出不等于其输入。

+

assert_eq!assert_ne!宏在底层分别使用了==!=。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了PartialEqDebug trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 PartialEq才能断言他们的值是否相等。需要实现 Debug才能在断言失败时打印他们的值。因为这两个 trait 都是可推导 trait,如第五章所提到的,通常可以直接在结构体或枚举上添加#[derive(PartialEq, Debug)]注解。附录 C 中有更多关于这些和其他可推导 trait 的详细信息。

+

自定义错误信息

+

也可以向assert!assert_eq!assert_ne!宏传递一个可选的参数来增加用于打印的自定义错误信息。任何在assert!必需的一个参数和assert_eq!assert_ne!必需的两个参数之后指定的参数都会传递给第八章讲到的format!宏,所以可以传递一个包含{}占位符的格式字符串和放入占位符的值。自定义信息有助于记录断言的意义,这样到测试失败时,就能更好的例子代码出了什么问题。

+

例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:

Filename: src/lib.rs

-
#[test]
-fn it_works() {
-    assert_eq!("Hello", "Hello");
-
-    assert_ne!("Hello", "world");
-}
-
-

也可以对这些宏指定可选的第三个参数,它是一个会加入错误信息的自定义文本。这两个宏展开后的逻辑看起来像这样:

-
// assert_eq! - panic if the values aren't equal
-if left_val != right_val {
-    panic!(
-        "assertion failed: `(left == right)` (left: `{:?}`, right: `{:?}`): {}"
-        left_val,
-        right_val,
-        optional_custom_message
-    )
+
pub fn greeting(name: &str) -> String {
+    format!("Hello {}!", name)
 }
 
-// assert_ne! - panic if the values are equal
-if left_val == right_val {
-    panic!(
-        "assertion failed: `(left != right)` (left: `{:?}`, right: `{:?}`): {}"
-        left_val,
-        right_val,
-        optional_custom_message
-    )
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn greeting_contains_name() {
+        let result = greeting("Carol");
+        assert!(result.contains("Carol"));
+    }
 }
 
-

看看这个因为hello不等于world而失败的测试。我们还增加了一个自定义的错误信息,greeting operation failed

+

这个程序的需求还没有被确定,而我们非常确定问候开始的Hello文本不会改变。我们决定并不想在人名改变时 +不得不更新测试,所以相比检查greeting函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。

+

让我们通过改变greeting不包含name来在代码中引入一个 bug 来测试失败时是怎样的,

+
pub fn greeting(name: &str) -> String {
+    String::from("Hello!")
+}
+
+

运行测试会产生:

+
running 1 test
+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
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+    tests::greeting_contains_name
+
+

这仅仅告诉了我们断言失败了和失败的行号。一个更有用的错误信息应该打印出从greeting函数得到的值。让我们改变测试函数来使用一个由包含占位符的格式字符串和从greeting函数取得的值组成的自定义错误信息:

+
#[test]
+fn greeting_contains_name() {
+    let result = greeting("Carol");
+    assert!(
+        result.contains("Carol"),
+        "Greeting did not contain name, value was `{}`", result
+    );
+}
+
+

现在如果再次运行测试,将会看到更有价值的错误信息:

+
---- tests::greeting_contains_name stdout ----
+    thread 'tests::greeting_contains_name' panicked at 'Result did not contain
+    name, value was `Hello`', src/lib.rs:12
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+

可以在测试输出中看到所取得的确切的值,这会帮助我们理解发生了什么而不是期望发生什么。

+

使用should_panic检查 panic

+

除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章列表 9-8 创建的Guess类型。其他使用Guess的代码依赖于Guess实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的Guess实例会 panic。

+

可以通过对函数增加另一个属性should_panic来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。

+

列表 11-8 展示了如何编写一个测试来检查Guess::new按照我们的期望出现的错误情况:

Filename: src/lib.rs

-
#[test]
-fn a_simple_case() {
-    let result = "hello"; // this value would come from running your code
-    assert_eq!(result, "world", "greeting operation failed");
+
struct Guess {
+    value: u32,
+}
+
+impl Guess {
+    pub fn new(value: u32) -> Guess {
+        if value < 1 || value > 100 {
+            panic!("Guess value must be between 1 and 100, got {}.", value);
+        }
+
+        Guess {
+            value: value,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    #[should_panic]
+    fn greater_than_100() {
+        Guess::new(200);
+    }
 }
 
-

毫无疑问运行这个测试会失败,而错误信息解释了为什么测试失败了并且带有我们的指定的自定义错误信息:

-
---- a_simple_case stdout ----
-    thread 'a_simple_case' panicked at 'assertion failed: `(left == right)`
-    (left: `"hello"`, right: `"world"`): greeting operation failed',
-    src/main.rs:4
+

Listing 11-8: Testing that a condition will cause a +panic!

+

#[should_panic]属性位于#[test]之后和对应的测试函数之前。让我们看看测试通过时它时什么样子:

+
running 1 test
+test tests::greater_than_100 ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
 
-

assert_eq!的两个参数被称为 "left" 和 "right" ,而不是 "expected" 和 "actual" ;值的顺序和硬编码的值并没有什么影响。

-

因为这些宏使用了==!=运算符并使用调试格式打印这些值,进行比较的值必须实现PartialEqDebug trait。Rust 提供的类型实现了这些 trait,不过自定义的结构体和枚举则需要自己实现PartialEq以便能够断言这些值是否相等,和实现Debug以便在断言失败时打印出这些值。因为第五章提到过这两个 trait 都是 derivable trait,所以通常可以直接在结构体或枚举上加上#[derive(PartialEq, Debug)]注解。查看附录 C 来寻找更多关于这些和其他 derivable trait 的信息。

-

使用should_panic测试期望的失败

-

可以使用另一个属性来反转测试中的失败:should_panic。这在测试调用特定的函数会产生错误的函数时很有帮助。例如,让我们测试第八章中的一些我们知道会 panic 的代码:尝试使用 range 语法和并不组成完整字母的字节索引来创建一个字符串 slice。在有#[test]属性的函数之前增加#[should_panic]属性,如列表 11-1 所示:

-
-Filename: src/lib.rs -
#[test]
-#[should_panic]
-fn slice_not_on_char_boundaries() {
-    let s = "Здравствуйте";
-    &s[0..1];
+

看起来不错!现在在代码中引入 bug,通过移除new函数在值大于 100 时会 panic 的条件:

+
# struct Guess {
+#     value: u32,
+# }
+#
+impl Guess {
+    pub fn new(value: u32) -> Guess {
+        if value < 1  {
+            panic!("Guess value must be between 1 and 100, got {}.", value);
+        }
+
+        Guess {
+            value: value,
+        }
+    }
 }
 
-
-

Listing 11-1: A test expecting a panic!

-
-
-

这个测试是成功的,因为我们表示代码应该会 panic。相反如果代码因为某种原因没有产生panic!则测试会失败。

-

使用should_panic的测试是脆弱的,因为难以保证测试不会因为一个不同于我们期望的原因失败。为了帮助解决这个问题,should_panic属性可以增加一个可选的expected参数。测试工具会确保错误信息里包含我们提供的文本。一个比列表 11-1 更健壮的版本如列表 11-2 所示:

-
-Filename: src/lib.rs -
#[test]
-#[should_panic(expected = "do not lie on character boundary")]
-fn slice_not_on_char_boundaries() {
-    let s = "Здравствуйте";
-    &s[0..1];
+

如果运行列表 11-8 的测试,它会失败:

+
running 1 test
+test tests::greater_than_100 ... FAILED
+
+failures:
+
+failures:
+    tests::greater_than_100
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+
+

这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了#[should_panic]。这个错误意味着代码中函数Guess::new(200)并没有产生 panic。

+

然而should_panic测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。should_panic甚至在测试因为其他不同的原因而不是我们期望发生的那个而 panic 时也会通过。为了使should_panic测试更精确,可以给should_panic属性增加一个可选的expected参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑列表 11-9 中修改过的Guess,这里new函数更具其值是过大还或者过小而提供不同的 panic 信息:

+

Filename: src/lib.rs

+
struct Guess {
+    value: u32,
+}
+
+impl Guess {
+    pub fn new(value: u32) -> Guess {
+        if value < 1 {
+            panic!("Guess value must be greater than or equal to 1, got {}.",
+                   value);
+        } else if value > 100 {
+            panic!("Guess value must be less than or equal to 100, got {}.",
+                   value);
+        }
+
+        Guess {
+            value: value,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    #[should_panic(expected = "Guess value must be less than or equal to 100")]
+    fn greater_than_100() {
+        Guess::new(200);
+    }
 }
 
- -
-

Listing 11-2: A test expecting a panic! with a particular message

-
-
-

请自行尝试当should_panic的测试出现 panic 但并不符合期望的信息时会发生什么:在测试中因为不同原因造成panic!,或者将期望的 panic 信息改为并不与字母字节边界 panic 信息相匹配。

+

Listing 11-9: Testing that a condition will cause a +panic! with a particular panic message

+

这个测试会通过,因为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 < 1else if value > 100的代码块对换:

+
if value < 1 {
+    panic!("Guess value must be less than or equal to 100, got {}.", value);
+} else if value > 100 {
+    panic!("Guess value must be greater than or equal to 1, got {}.", value);
+}
+
+

这一次运行should_panic测试,它会失败:

+
running 1 test
+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
+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'
+
+failures:
+    tests::greater_than_100
+
+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 在哪了!

+

现在我们讲完了编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于cargo test的不同选项。

diff --git a/docs/print.html b/docs/print.html index 967a5f7..4e99b8d 100644 --- a/docs/print.html +++ b/docs/print.html @@ -1545,8 +1545,7 @@ the value is: 50

所有数组中的五个元素都如期被打印出来。尽管index在某一时刻会到达值5,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。

不过这个过程是容易出错的;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。

可以使用for循环来对一个集合的每个元素执行一些代码,来作为一个更有效率替代。for循环看起来像这样:

-
-Filename: src/main.rs +

Filename: src/main.rs

fn main() {
     let a = [10, 20, 30, 40, 50];
 
@@ -1555,10 +1554,8 @@ the value is: 50
     }
 }
 
-
-

Listing 3-6: Looping through each element of a collection using a for loop

-
-
+

Listing 3-6: Looping through each element of a collection +using a for loop

当运行这段代码,将看到与列表 3-5 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 机会。

例如,在列表 3-5 的代码中,如果从数组a中移除一个元素但忘记更新条件为while index < 4,代码将会 panic。使用for循环的话,就不需要惦记着在更新数组元素数量时修改其他的代码了。

for循环的安全性和简洁性使得它在成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如列表 3-5 中使用while循环的倒计时例子,大部分 Rustacean 也会使用for循环。这么做的方式是使用Range,它是标准库提供的用来生成从一个数字开始到另一个数字结束的所有数字序列的类型。

@@ -3179,7 +3176,7 @@ mod server { }

注意这个模块文件中我们也使用了一个mod声明;这是因为我们希望server成为network的一个子模块。

-

现在再次运行cargo build。成功!不过我们还需要再提取出另一个模块:server。因为这是一个子模块————也就是模块中的模块————目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;替换server模块的内容:

+

现在再次运行cargo build。成功!不过我们还需要再提取出另一个模块:server。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;替换server模块的内容:

Filename: src/network.rs

fn connect() {
 }
@@ -5582,133 +5579,285 @@ mod tests {
     }
 }
 
-

Cargo 编译并运行了测试。这里有两部分输出:本章我们将关注第一部分。第二部分是文档测试的输出,第十四章会介绍他们。现在注意看这一行:

-
test it_works ... ok
+

因为这里can_hold函数的正确结果是false,我们需要将这个结果取反后传递给assert!宏。这样的话,测试就会通过而can_hold将返回false

+
running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
 
-

it_works文本来源于测试函数的名称。

-

这里也有一行总结告诉我们所有测试的聚合结果:

-
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-

assert!

-

空的测试函数之所以能通过是因为任何没有panic!的测试都是通过的,而任何panic!的测试都算是失败。让我们使用`assert!宏来使测试失败:

-

Filename: src/lib.rs

-
#[test]
-fn it_works() {
-    assert!(false);
+

这个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将can_hold方法中比较长度时本应使用大于号的地方改成小于号:

+
#[derive(Debug)]
+pub struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+impl Rectangle {
+    pub fn can_hold(&self, other: &Rectangle) -> bool {
+        self.length < other.length && self.width > other.width
+    }
 }
 
-

assert!宏由标准库提供,它获取一个参数,如果参数是true,什么也不会发生。如果参数是false,这个宏会panic!。再次运行测试:

-
$ cargo test
-   Compiling adder v0.1.0 (file:///projects/adder)
-    Finished debug [unoptimized + debuginfo] target(s) in 0.22 secs
-     Running target/debug/deps/adder-abcabcabc
-
-running 1 test
-test it_works ... FAILED
+

现在运行测试会产生:

+
running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... FAILED
 
 failures:
 
----- it_works stdout ----
-    thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
+---- tests::larger_can_hold_smaller stdout ----
+    thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed:
+    larger.can_hold(&smaller)', src/lib.rs:22
 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
+
+

我们的测试捕获了 bug!因为larger.length是 8 而smaller.length 是 5,can_hold中的长度比较现在返回false因为 8 不小于 5。

+

使用assert_eq!assert_ne!宏来测试相等

+

测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向assert!宏传递一个使用==宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来方便处理这些操作:assert_eq!assert_ne!。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试为什么失败,而assert!只会打印出它从==表达式中得到了false值,而不是导致false值的原因。

+

列表 11-7 中,让我们编写一个对其参数加二并返回结果的函数add_two。接着使用assert_eq!宏测试这个函数:

+

Filename: src/lib.rs

+
pub fn add_two(a: i32) -> i32 {
+    a + 2
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn it_adds_two() {
+        assert_eq!(4, add_two(2));
+    }
+}
+
+

Listing 11-7: Testing the function add_two using the +assert_eq! macro

+

测试通过了!

+
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 ... okok表明测试通过了!

+

在代码中引入一个 bug 来看看使用assert_eq!的测试失败是什么样的。修改add_two函数的实现使其加 3:

+
pub fn add_two(a: i32) -> i32 {
+    a + 3
+}
+
+

再次运行测试:

+
running 1 test
+test tests::it_adds_two ... FAILED
 
 failures:
-    it_works
+
+---- tests::it_adds_two stdout ----
+    thread 'tests::it_adds_two' panicked at 'assertion failed: `(left ==
+    right)` (left: `4`, right: `5`)', src/lib.rs:11
+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
-
-error: test failed
 
-

Rust 表明测试失败了:

-
test it_works ... FAILED
-
-

并展示了测试是因为src/lib.rs的第 5 行assert!宏得到了一个false`值而失败的:

-
thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
-
-

失败的测试也体现在了总结行中:

-
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
-
-

使用assert_eq!assert_ne!宏来测试相等

-

测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向assert!宏传递一个使用==宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来编译处理这些操作:assert_eq!assert_ne!。这两个宏分别比较两个值是相等还是不相等。使用这些宏的另一个优势是当断言失败时他们会打印出这两个值具体是什么,以便于观察测试为什么失败,而assert!只会打印出它从==表达式中得到了false值。

-

下面是分别使用这两个宏其会测试通过的例子:

+

测试捕获到了 bug!it_adds_two测试失败并显示信息assertion failed: `(left == right)` (left: `4`, right: `5`)。这个信息有助于我们开始调试:它说assert_eq!left参数是 4,而right参数,也就是add_two(2)的结果,是 5。

+

注意在一些语言和测试框架中,断言两个值相等的函数的参数叫做expectedactual,而且指定参数的顺序是需要注意的。然而在 Rust 中,他们则叫做leftright,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成assert_eq!(add_two(2), 4),这时错误信息会变成assertion failed: `(left == right)` (left: `5`, right: `4`)

+

assert_ne!宏在传递给它的两个值不相等时通过而在相等时失败。这个宏在代码按照我们期望运行时不确定值应该是什么,不过知道他们绝对不应该是什么的时候最有用处。例如,如果一个函数确定会以某种方式改变其输出,不过这种方式由运行测试是星期几来决定,这时最好的断言可能就是函数的输出不等于其输入。

+

assert_eq!assert_ne!宏在底层分别使用了==!=。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了PartialEqDebug trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 PartialEq才能断言他们的值是否相等。需要实现 Debug才能在断言失败时打印他们的值。因为这两个 trait 都是可推导 trait,如第五章所提到的,通常可以直接在结构体或枚举上添加#[derive(PartialEq, Debug)]注解。附录 C 中有更多关于这些和其他可推导 trait 的详细信息。

+

自定义错误信息

+

也可以向assert!assert_eq!assert_ne!宏传递一个可选的参数来增加用于打印的自定义错误信息。任何在assert!必需的一个参数和assert_eq!assert_ne!必需的两个参数之后指定的参数都会传递给第八章讲到的format!宏,所以可以传递一个包含{}占位符的格式字符串和放入占位符的值。自定义信息有助于记录断言的意义,这样到测试失败时,就能更好的例子代码出了什么问题。

+

例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:

Filename: src/lib.rs

-
#[test]
-fn it_works() {
-    assert_eq!("Hello", "Hello");
-
-    assert_ne!("Hello", "world");
-}
-
-

也可以对这些宏指定可选的第三个参数,它是一个会加入错误信息的自定义文本。这两个宏展开后的逻辑看起来像这样:

-
// assert_eq! - panic if the values aren't equal
-if left_val != right_val {
-    panic!(
-        "assertion failed: `(left == right)` (left: `{:?}`, right: `{:?}`): {}"
-        left_val,
-        right_val,
-        optional_custom_message
-    )
+
pub fn greeting(name: &str) -> String {
+    format!("Hello {}!", name)
 }
 
-// assert_ne! - panic if the values are equal
-if left_val == right_val {
-    panic!(
-        "assertion failed: `(left != right)` (left: `{:?}`, right: `{:?}`): {}"
-        left_val,
-        right_val,
-        optional_custom_message
-    )
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn greeting_contains_name() {
+        let result = greeting("Carol");
+        assert!(result.contains("Carol"));
+    }
 }
 
-

看看这个因为hello不等于world而失败的测试。我们还增加了一个自定义的错误信息,greeting operation failed

+

这个程序的需求还没有被确定,而我们非常确定问候开始的Hello文本不会改变。我们决定并不想在人名改变时 +不得不更新测试,所以相比检查greeting函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。

+

让我们通过改变greeting不包含name来在代码中引入一个 bug 来测试失败时是怎样的,

+
pub fn greeting(name: &str) -> String {
+    String::from("Hello!")
+}
+
+

运行测试会产生:

+
running 1 test
+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
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+    tests::greeting_contains_name
+
+

这仅仅告诉了我们断言失败了和失败的行号。一个更有用的错误信息应该打印出从greeting函数得到的值。让我们改变测试函数来使用一个由包含占位符的格式字符串和从greeting函数取得的值组成的自定义错误信息:

+
#[test]
+fn greeting_contains_name() {
+    let result = greeting("Carol");
+    assert!(
+        result.contains("Carol"),
+        "Greeting did not contain name, value was `{}`", result
+    );
+}
+
+

现在如果再次运行测试,将会看到更有价值的错误信息:

+
---- tests::greeting_contains_name stdout ----
+    thread 'tests::greeting_contains_name' panicked at 'Result did not contain
+    name, value was `Hello`', src/lib.rs:12
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+

可以在测试输出中看到所取得的确切的值,这会帮助我们理解发生了什么而不是期望发生什么。

+

使用should_panic检查 panic

+

除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章列表 9-8 创建的Guess类型。其他使用Guess的代码依赖于Guess实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的Guess实例会 panic。

+

可以通过对函数增加另一个属性should_panic来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。

+

列表 11-8 展示了如何编写一个测试来检查Guess::new按照我们的期望出现的错误情况:

Filename: src/lib.rs

-
#[test]
-fn a_simple_case() {
-    let result = "hello"; // this value would come from running your code
-    assert_eq!(result, "world", "greeting operation failed");
+
struct Guess {
+    value: u32,
+}
+
+impl Guess {
+    pub fn new(value: u32) -> Guess {
+        if value < 1 || value > 100 {
+            panic!("Guess value must be between 1 and 100, got {}.", value);
+        }
+
+        Guess {
+            value: value,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    #[should_panic]
+    fn greater_than_100() {
+        Guess::new(200);
+    }
 }
 
-

毫无疑问运行这个测试会失败,而错误信息解释了为什么测试失败了并且带有我们的指定的自定义错误信息:

-
---- a_simple_case stdout ----
-    thread 'a_simple_case' panicked at 'assertion failed: `(left == right)`
-    (left: `"hello"`, right: `"world"`): greeting operation failed',
-    src/main.rs:4
+

Listing 11-8: Testing that a condition will cause a +panic!

+

#[should_panic]属性位于#[test]之后和对应的测试函数之前。让我们看看测试通过时它时什么样子:

+
running 1 test
+test tests::greater_than_100 ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
 
-

assert_eq!的两个参数被称为 "left" 和 "right" ,而不是 "expected" 和 "actual" ;值的顺序和硬编码的值并没有什么影响。

-

因为这些宏使用了==!=运算符并使用调试格式打印这些值,进行比较的值必须实现PartialEqDebug trait。Rust 提供的类型实现了这些 trait,不过自定义的结构体和枚举则需要自己实现PartialEq以便能够断言这些值是否相等,和实现Debug以便在断言失败时打印出这些值。因为第五章提到过这两个 trait 都是 derivable trait,所以通常可以直接在结构体或枚举上加上#[derive(PartialEq, Debug)]注解。查看附录 C 来寻找更多关于这些和其他 derivable trait 的信息。

-

使用should_panic测试期望的失败

-

可以使用另一个属性来反转测试中的失败:should_panic。这在测试调用特定的函数会产生错误的函数时很有帮助。例如,让我们测试第八章中的一些我们知道会 panic 的代码:尝试使用 range 语法和并不组成完整字母的字节索引来创建一个字符串 slice。在有#[test]属性的函数之前增加#[should_panic]属性,如列表 11-1 所示:

-
-Filename: src/lib.rs -
#[test]
-#[should_panic]
-fn slice_not_on_char_boundaries() {
-    let s = "Здравствуйте";
-    &s[0..1];
+

看起来不错!现在在代码中引入 bug,通过移除new函数在值大于 100 时会 panic 的条件:

+
# struct Guess {
+#     value: u32,
+# }
+#
+impl Guess {
+    pub fn new(value: u32) -> Guess {
+        if value < 1  {
+            panic!("Guess value must be between 1 and 100, got {}.", value);
+        }
+
+        Guess {
+            value: value,
+        }
+    }
 }
 
-
-

Listing 11-1: A test expecting a panic!

-
-
-

这个测试是成功的,因为我们表示代码应该会 panic。相反如果代码因为某种原因没有产生panic!则测试会失败。

-

使用should_panic的测试是脆弱的,因为难以保证测试不会因为一个不同于我们期望的原因失败。为了帮助解决这个问题,should_panic属性可以增加一个可选的expected参数。测试工具会确保错误信息里包含我们提供的文本。一个比列表 11-1 更健壮的版本如列表 11-2 所示:

-
-Filename: src/lib.rs -
#[test]
-#[should_panic(expected = "do not lie on character boundary")]
-fn slice_not_on_char_boundaries() {
-    let s = "Здравствуйте";
-    &s[0..1];
+

如果运行列表 11-8 的测试,它会失败:

+
running 1 test
+test tests::greater_than_100 ... FAILED
+
+failures:
+
+failures:
+    tests::greater_than_100
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+
+

这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了#[should_panic]。这个错误意味着代码中函数Guess::new(200)并没有产生 panic。

+

然而should_panic测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。should_panic甚至在测试因为其他不同的原因而不是我们期望发生的那个而 panic 时也会通过。为了使should_panic测试更精确,可以给should_panic属性增加一个可选的expected参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑列表 11-9 中修改过的Guess,这里new函数更具其值是过大还或者过小而提供不同的 panic 信息:

+

Filename: src/lib.rs

+
struct Guess {
+    value: u32,
+}
+
+impl Guess {
+    pub fn new(value: u32) -> Guess {
+        if value < 1 {
+            panic!("Guess value must be greater than or equal to 1, got {}.",
+                   value);
+        } else if value > 100 {
+            panic!("Guess value must be less than or equal to 100, got {}.",
+                   value);
+        }
+
+        Guess {
+            value: value,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    #[should_panic(expected = "Guess value must be less than or equal to 100")]
+    fn greater_than_100() {
+        Guess::new(200);
+    }
 }
 
- -
-

Listing 11-2: A test expecting a panic! with a particular message

-
-
-

请自行尝试当should_panic的测试出现 panic 但并不符合期望的信息时会发生什么:在测试中因为不同原因造成panic!,或者将期望的 panic 信息改为并不与字母字节边界 panic 信息相匹配。

+

Listing 11-9: Testing that a condition will cause a +panic! with a particular panic message

+

这个测试会通过,因为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 < 1else if value > 100的代码块对换:

+
if value < 1 {
+    panic!("Guess value must be less than or equal to 100, got {}.", value);
+} else if value > 100 {
+    panic!("Guess value must be greater than or equal to 1, got {}.", value);
+}
+
+

这一次运行should_panic测试,它会失败:

+
running 1 test
+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
+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'
+
+failures:
+    tests::greater_than_100
+
+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 在哪了!

+

现在我们讲完了编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于cargo test的不同选项。

运行测试

ch11-02-running-tests.md diff --git a/src/ch03-05-control-flow.md b/src/ch03-05-control-flow.md index 0c17184..6fb1f6d 100644 --- a/src/ch03-05-control-flow.md +++ b/src/ch03-05-control-flow.md @@ -305,7 +305,6 @@ the value is: 50 可以使用`for`循环来对一个集合的每个元素执行一些代码,来作为一个更有效率替代。`for`循环看起来像这样: -

Filename: src/main.rs ```rust @@ -318,12 +317,8 @@ fn main() { } ``` -
- -Listing 3-6: Looping through each element of a collection using a `for` loop - -
-
+Listing 3-6: Looping through each element of a collection +using a `for` loop 当运行这段代码,将看到与列表 3-5 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 机会。 diff --git a/src/ch07-01-mod-and-the-filesystem.md b/src/ch07-01-mod-and-the-filesystem.md index 945188c..1b7a958 100644 --- a/src/ch07-01-mod-and-the-filesystem.md +++ b/src/ch07-01-mod-and-the-filesystem.md @@ -230,7 +230,7 @@ mod server { 注意这个模块文件中我们也使用了一个`mod`声明;这是因为我们希望`server`成为`network`的一个子模块。 -现在再次运行`cargo build`。成功!不过我们还需要再提取出另一个模块:`server`。因为这是一个子模块————也就是模块中的模块————目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 *src/network.rs* 的第一个修改是用`mod server;`替换`server`模块的内容: +现在再次运行`cargo build`。成功!不过我们还需要再提取出另一个模块:`server`。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 *src/network.rs* 的第一个修改是用`mod server;`替换`server`模块的内容: Filename: src/network.rs diff --git a/src/ch11-01-writing-tests.md b/src/ch11-01-writing-tests.md index d7fe508..a03b4b7 100644 --- a/src/ch11-01-writing-tests.md +++ b/src/ch11-01-writing-tests.md @@ -169,7 +169,6 @@ test fails `assert!`宏由标准库提供,在希望确保测试中一些条件为`true`时非常有用。需要向`assert!`宏提供一个计算为布尔值的参数。如果值是`true`,`assert!`什么也不做同时测试会通过。如果值为`false`,`assert!`调用`panic!`宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。 - +Listing 11-8: Testing that a condition will cause a +`panic!` -
+`#[should_panic]`属性位于`#[test]`之后和对应的测试函数之前。让我们看看测试通过时它时什么样子: -Listing 11-2: A test expecting a `panic!` with a particular message +``` +running 1 test +test tests::greater_than_100 ... ok -
- +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +``` -请自行尝试当`should_panic`的测试出现 panic 但并不符合期望的信息时会发生什么:在测试中因为不同原因造成`panic!`,或者将期望的 panic 信息改为并不与字母字节边界 panic 信息相匹配。 \ No newline at end of file +看起来不错!现在在代码中引入 bug,通过移除`new`函数在值大于 100 时会 panic 的条件: + +```rust +# struct Guess { +# value: u32, +# } +# +impl Guess { + pub fn new(value: u32) -> Guess { + if value < 1 { + panic!("Guess value must be between 1 and 100, got {}.", value); + } + + Guess { + value: value, + } + } +} +``` + +如果运行列表 11-8 的测试,它会失败: + +``` +running 1 test +test tests::greater_than_100 ... FAILED + +failures: + +failures: + tests::greater_than_100 + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured +``` + +这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了`#[should_panic]`。这个错误意味着代码中函数`Guess::new(200)`并没有产生 panic。 + +然而`should_panic`测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。`should_panic`甚至在测试因为其他不同的原因而不是我们期望发生的那个而 panic 时也会通过。为了使`should_panic`测试更精确,可以给`should_panic`属性增加一个可选的`expected`参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑列表 11-9 中修改过的`Guess`,这里`new`函数更具其值是过大还或者过小而提供不同的 panic 信息: + +Filename: src/lib.rs + +```rust +struct Guess { + value: u32, +} + +impl Guess { + pub fn new(value: u32) -> Guess { + if value < 1 { + panic!("Guess value must be greater than or equal to 1, got {}.", + value); + } else if value > 100 { + panic!("Guess value must be less than or equal to 100, got {}.", + value); + } + + Guess { + value: value, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "Guess value must be less than or equal to 100")] + fn greater_than_100() { + Guess::new(200); + } +} +``` + +Listing 11-9: Testing that a condition will cause a +`panic!` with a particular panic message + +这个测试会通过,因为`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`的代码块对换: + +```rust,ignore +if value < 1 { + panic!("Guess value must be less than or equal to 100, got {}.", value); +} else if value > 100 { + panic!("Guess value must be greater than or equal to 1, got {}.", value); +} +``` + +这一次运行`should_panic`测试,它会失败: + +``` +running 1 test +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 +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' + +failures: + tests::greater_than_100 + +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 在哪了! + +现在我们讲完了编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于`cargo test`的不同选项。 \ No newline at end of file