diff --git a/docs/ch11-01-writing-tests.html b/docs/ch11-01-writing-tests.html index 2c6d047..2e87491 100644 --- a/docs/ch11-01-writing-tests.html +++ b/docs/ch11-01-writing-tests.html @@ -73,6 +73,166 @@
commit 77370c073661548dd56bbcb43cc64713585acbba

+

测试是一种使用特定功能的 Rust 函数,它用来验证非测试的代码按照期望的方式运行。我们讨论过的任何 Rust 代码规则都适用于测试!让我们看看 Rust 提供的具体用来编写测试的功能:test属性、一些宏和should_panic属性。

+

test属性

+

作为最简单例子,Rust 中的测试就是一个带有test属性注解的函数。让我们使用 Cargo 来创建一个新的库项目adder

+
$ cargo new adder
+     Created library `adder` project
+$ cd adder
+
+

Cargo 在创建新的库项目时自动生成一个简单的测试。这是src/lib.rs中的内容:

+

Filename: src/lib.rs

+
#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+    }
+}
+
+

现在让我们暂时忽略tests模块和#[cfg(test)]注解并只关注函数。注意它之前的#[test]:这个属性表明这是一个测试函数。这个函数目前没有任何内容,所以绝对是可以通过的!使用cargo test来运行测试:

+
$ 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 ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+   Doc-tests adder
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+

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

+
test it_works ... ok
+
+

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);
+}
+
+

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
+
+failures:
+
+---- it_works stdout ----
+    thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+
+failures:
+    it_works
+
+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值。

+

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

+

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
+    )
+}
+
+// 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
+    )
+}
+
+

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

+

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");
+}
+
+

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

+
---- 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
+
+

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];
+}
+
+
+

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];
+}
+
+ +
+

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

+
+
+

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

diff --git a/docs/ch11-02-running-tests.html b/docs/ch11-02-running-tests.html index 798234d..2dd650f 100644 --- a/docs/ch11-02-running-tests.html +++ b/docs/ch11-02-running-tests.html @@ -67,7 +67,178 @@
- +

运行测试

+
+

ch11-02-running-tests.md +
+commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc

+
+

类似于cargo run会编译代码并运行生成的二进制文件,cargo test在测试模式下编译代码并运行生成的测试二进制文件。cargo test生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。

+

可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给cargo test,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是--cargo test之后列出了传递给cargo test的参数,接着是分隔符--,之后是传递给测试二进制文件的参数。

+

并行运行测试

+

测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。

+

如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作:

+
$ cargo test -- --test-threads=1
+
+

捕获测试输出

+

Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了println!而测试通过了,你将不会在终端看到println!的输出。这个行为可以通过向测试二进制文件传递--nocapture参数来禁用:

+
$ cargo test -- --nocapture
+
+

通过名称来运行测试的子集

+

有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。cargo test有一个参数允许你通过指定名称来运行特定的测试。

+

列表 11-3 中创建了三个如下名称的测试:

+
+Filename: src/lib.rs +
#[test]
+fn add_two_and_two() {
+    assert_eq!(4, 2 + 2);
+}
+
+#[test]
+fn add_three_and_two() {
+    assert_eq!(5, 3 + 2);
+}
+
+#[test]
+fn one_hundred() {
+    assert_eq!(102, 100 + 2);
+}
+
+
+

Listing 11-3: Three tests with a variety of names

+
+
+

使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试:

+
$ cargo test
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 3 tests
+test add_three_and_two ... ok
+test one_hundred ... ok
+test add_two_and_two ... ok
+
+test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
+
+

可以传递任意测试的名称来只运行那个测试:

+
$ cargo test one_hundred
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 1 test
+test one_hundred ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+

也可以传递名称的一部分,cargo test会运行所有匹配的测试:

+
$ cargo test add
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 2 tests
+test add_three_and_two ... ok
+test add_two_and_two ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
+
+

模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫adding的模块和一个叫subtracting的模块并分别带有测试,如列表 11-4 所示:

+
+Filename: src/lib.rs +
mod adding {
+    #[test]
+    fn add_two_and_two() {
+        assert_eq!(4, 2 + 2);
+    }
+
+    #[test]
+    fn add_three_and_two() {
+        assert_eq!(5, 3 + 2);
+    }
+
+    #[test]
+    fn one_hundred() {
+        assert_eq!(102, 100 + 2);
+    }
+}
+
+mod subtracting {
+    #[test]
+    fn subtract_three_and_two() {
+        assert_eq!(1, 3 - 2);
+    }
+}
+
+
+

Listing 11-4: Tests in two modules named adding and subtracting

+
+
+

执行cargo test会运行所有的测试,而模块名会出现在输出的测试名中:

+
$ cargo test
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 4 tests
+test adding::add_two_and_two ... ok
+test adding::add_three_and_two ... ok
+test subtracting::subtract_three_and_two ... ok
+test adding::one_hundred ... ok
+
+

运行cargo test adding将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试:

+
$ cargo test adding
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 3 tests
+test adding::add_three_and_two ... ok
+test adding::one_hundred ... ok
+test adding::add_two_and_two ... ok
+
+test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
+
+

除非指定否则忽略某些测试

+

有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数cargo test命令,我们希望能排除它。无需为cargo test创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用ignore属性:

+

Filename: src/lib.rs

+
#[test]
+fn it_works() {
+    assert!(true);
+}
+
+#[test]
+#[ignore]
+fn expensive_test() {
+    // code that takes an hour to run
+}
+
+

现在运行测试,将会发现it_works运行了,而expensive_test没有:

+
$ cargo test
+   Compiling adder v0.1.0 (file:///projects/adder)
+    Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
+     Running target/debug/deps/adder-abcabcabc
+
+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
+
+

我们可以通过cargo test -- --ignored来明确请求只运行那些耗时的测试:

+
$ cargo test -- --ignored
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 1 test
+test expensive_test ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+

通过这种方式,大部分时间运行cargo test将是快速的。当需要检查ignored测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored

+
diff --git a/docs/ch11-03-test-organization.html b/docs/ch11-03-test-organization.html index 9f319cf..b4d867e 100644 --- a/docs/ch11-03-test-organization.html +++ b/docs/ch11-03-test-organization.html @@ -67,7 +67,93 @@
- +

测试的组织结构

+
+

ch11-03-test-organization.md +
+commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc

+
+

正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

+

单元测试

+

单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的tests模块中。

+

测试模块和cfg(test)

+

通过将测试放进他们自己的模块并对该模块使用cfg注解,我们可以告诉 Rust 只在执行cargo test时才编译和运行测试代码。这在当我们只希望用cargo build编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。

+

还记得上一部分新建的adder项目吗?Cargo 为我们生成了如下代码:

+

Filename: src/lib.rs

+
#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+    }
+}
+
+

我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。

+

首先,这里有一个属性cfgcfg属性让我们声明一些内容只在给定特定的配置configuration)时才被包含进来。Rust 提供了test配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。

+

接下来,tests包含了所有测试函数,而我们的代码则位于tests模块之外。tests模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。

+

从本章到现在,我们一直在为adder项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 src/lib.rs 中,放入add_two函数和带有一个检验代码的测试的tests模块,如列表 11-5 所示:

+
+Filename: src/lib.rs +
pub fn add_two(a: i32) -> i32 {
+    a + 2
+}
+
+#[cfg(test)]
+mod tests {
+    use add_two;
+
+    #[test]
+    fn it_works() {
+        assert_eq!(4, add_two(2));
+    }
+}
+
+
+

Listing 11-5: Testing the function add_two in a child tests module

+
+
+

注意除了测试函数之外,我们还在tests模块中添加了use add_two;。这将我们想要测试的代码引入到了内部的tests模块的作用域中,正如任何内部模块需要做的那样。如果现在使用cargo test运行测试,它会通过:

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

如果我们忘记将add_two函数引入作用域,将会得到一个 unresolved name 错误,因为tests模块并不知道任何关于add_two函数的信息:

+
error[E0425]: unresolved name `add_two`
+ --> src/lib.rs:9:23
+  |
+9 |         assert_eq!(4, add_two(2));
+  |                       ^^^^^^^ unresolved name
+
+

如果这个模块包含很多希望测试的代码,在测试中列出每一个use语句将是很烦人的。相反在测试子模块中使用use super::*;来一次将所有内容导入作用域中是很常见的。

+

测试私有函数

+

测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数internal_adder的代码:

+
+Filename: src/lib.rs +
pub fn add_two(a: i32) -> i32 {
+    internal_adder(a, 2)
+}
+
+fn internal_adder(a: i32, b: i32) -> i32 {
+    a + b
+}
+
+#[cfg(test)]
+mod tests {
+    use internal_adder;
+
+    #[test]
+    fn internal() {
+        assert_eq!(4, internal_adder(2, 2));
+    }
+}
+
+
+

Listing 11-6: Testing a private function

+
+
+

因为测试也不过是 Rust 代码而tests也只是另一个模块,我们完全可以在一个测试中导入并调用internal_adder。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。

+

集成测试

+
diff --git a/docs/print.html b/docs/print.html index 5a9a08f..bf98b58 100644 --- a/docs/print.html +++ b/docs/print.html @@ -5621,6 +5621,423 @@ commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c


commit 77370c073661548dd56bbcb43cc64713585acbba

+

测试是一种使用特定功能的 Rust 函数,它用来验证非测试的代码按照期望的方式运行。我们讨论过的任何 Rust 代码规则都适用于测试!让我们看看 Rust 提供的具体用来编写测试的功能:test属性、一些宏和should_panic属性。

+

test属性

+

作为最简单例子,Rust 中的测试就是一个带有test属性注解的函数。让我们使用 Cargo 来创建一个新的库项目adder

+
$ cargo new adder
+     Created library `adder` project
+$ cd adder
+
+

Cargo 在创建新的库项目时自动生成一个简单的测试。这是src/lib.rs中的内容:

+

Filename: src/lib.rs

+
#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+    }
+}
+
+

现在让我们暂时忽略tests模块和#[cfg(test)]注解并只关注函数。注意它之前的#[test]:这个属性表明这是一个测试函数。这个函数目前没有任何内容,所以绝对是可以通过的!使用cargo test来运行测试:

+
$ 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 ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+   Doc-tests adder
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+

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

+
test it_works ... ok
+
+

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);
+}
+
+

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
+
+failures:
+
+---- it_works stdout ----
+    thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+
+failures:
+    it_works
+
+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值。

+

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

+

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
+    )
+}
+
+// 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
+    )
+}
+
+

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

+

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");
+}
+
+

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

+
---- 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
+
+

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];
+}
+
+
+

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];
+}
+
+ +
+

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

+
+
+

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

+

运行测试

+
+

ch11-02-running-tests.md +
+commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc

+
+

类似于cargo run会编译代码并运行生成的二进制文件,cargo test在测试模式下编译代码并运行生成的测试二进制文件。cargo test生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。

+

可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给cargo test,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是--cargo test之后列出了传递给cargo test的参数,接着是分隔符--,之后是传递给测试二进制文件的参数。

+

并行运行测试

+

测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。

+

如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作:

+
$ cargo test -- --test-threads=1
+
+

捕获测试输出

+

Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了println!而测试通过了,你将不会在终端看到println!的输出。这个行为可以通过向测试二进制文件传递--nocapture参数来禁用:

+
$ cargo test -- --nocapture
+
+

通过名称来运行测试的子集

+

有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。cargo test有一个参数允许你通过指定名称来运行特定的测试。

+

列表 11-3 中创建了三个如下名称的测试:

+
+Filename: src/lib.rs +
#[test]
+fn add_two_and_two() {
+    assert_eq!(4, 2 + 2);
+}
+
+#[test]
+fn add_three_and_two() {
+    assert_eq!(5, 3 + 2);
+}
+
+#[test]
+fn one_hundred() {
+    assert_eq!(102, 100 + 2);
+}
+
+
+

Listing 11-3: Three tests with a variety of names

+
+
+

使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试:

+
$ cargo test
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 3 tests
+test add_three_and_two ... ok
+test one_hundred ... ok
+test add_two_and_two ... ok
+
+test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
+
+

可以传递任意测试的名称来只运行那个测试:

+
$ cargo test one_hundred
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 1 test
+test one_hundred ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+

也可以传递名称的一部分,cargo test会运行所有匹配的测试:

+
$ cargo test add
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 2 tests
+test add_three_and_two ... ok
+test add_two_and_two ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
+
+

模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫adding的模块和一个叫subtracting的模块并分别带有测试,如列表 11-4 所示:

+
+Filename: src/lib.rs +
mod adding {
+    #[test]
+    fn add_two_and_two() {
+        assert_eq!(4, 2 + 2);
+    }
+
+    #[test]
+    fn add_three_and_two() {
+        assert_eq!(5, 3 + 2);
+    }
+
+    #[test]
+    fn one_hundred() {
+        assert_eq!(102, 100 + 2);
+    }
+}
+
+mod subtracting {
+    #[test]
+    fn subtract_three_and_two() {
+        assert_eq!(1, 3 - 2);
+    }
+}
+
+
+

Listing 11-4: Tests in two modules named adding and subtracting

+
+
+

执行cargo test会运行所有的测试,而模块名会出现在输出的测试名中:

+
$ cargo test
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 4 tests
+test adding::add_two_and_two ... ok
+test adding::add_three_and_two ... ok
+test subtracting::subtract_three_and_two ... ok
+test adding::one_hundred ... ok
+
+

运行cargo test adding将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试:

+
$ cargo test adding
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 3 tests
+test adding::add_three_and_two ... ok
+test adding::one_hundred ... ok
+test adding::add_two_and_two ... ok
+
+test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
+
+

除非指定否则忽略某些测试

+

有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数cargo test命令,我们希望能排除它。无需为cargo test创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用ignore属性:

+

Filename: src/lib.rs

+
#[test]
+fn it_works() {
+    assert!(true);
+}
+
+#[test]
+#[ignore]
+fn expensive_test() {
+    // code that takes an hour to run
+}
+
+

现在运行测试,将会发现it_works运行了,而expensive_test没有:

+
$ cargo test
+   Compiling adder v0.1.0 (file:///projects/adder)
+    Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
+     Running target/debug/deps/adder-abcabcabc
+
+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
+
+

我们可以通过cargo test -- --ignored来明确请求只运行那些耗时的测试:

+
$ cargo test -- --ignored
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-abcabcabc
+
+running 1 test
+test expensive_test ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+

通过这种方式,大部分时间运行cargo test将是快速的。当需要检查ignored测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored

+

测试的组织结构

+
+

ch11-03-test-organization.md +
+commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc

+
+

正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

+

单元测试

+

单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的tests模块中。

+

测试模块和cfg(test)

+

通过将测试放进他们自己的模块并对该模块使用cfg注解,我们可以告诉 Rust 只在执行cargo test时才编译和运行测试代码。这在当我们只希望用cargo build编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。

+

还记得上一部分新建的adder项目吗?Cargo 为我们生成了如下代码:

+

Filename: src/lib.rs

+
#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+    }
+}
+
+

我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。

+

首先,这里有一个属性cfgcfg属性让我们声明一些内容只在给定特定的配置configuration)时才被包含进来。Rust 提供了test配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。

+

接下来,tests包含了所有测试函数,而我们的代码则位于tests模块之外。tests模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。

+

从本章到现在,我们一直在为adder项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 src/lib.rs 中,放入add_two函数和带有一个检验代码的测试的tests模块,如列表 11-5 所示:

+
+Filename: src/lib.rs +
pub fn add_two(a: i32) -> i32 {
+    a + 2
+}
+
+#[cfg(test)]
+mod tests {
+    use add_two;
+
+    #[test]
+    fn it_works() {
+        assert_eq!(4, add_two(2));
+    }
+}
+
+
+

Listing 11-5: Testing the function add_two in a child tests module

+
+
+

注意除了测试函数之外,我们还在tests模块中添加了use add_two;。这将我们想要测试的代码引入到了内部的tests模块的作用域中,正如任何内部模块需要做的那样。如果现在使用cargo test运行测试,它会通过:

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

如果我们忘记将add_two函数引入作用域,将会得到一个 unresolved name 错误,因为tests模块并不知道任何关于add_two函数的信息:

+
error[E0425]: unresolved name `add_two`
+ --> src/lib.rs:9:23
+  |
+9 |         assert_eq!(4, add_two(2));
+  |                       ^^^^^^^ unresolved name
+
+

如果这个模块包含很多希望测试的代码,在测试中列出每一个use语句将是很烦人的。相反在测试子模块中使用use super::*;来一次将所有内容导入作用域中是很常见的。

+

测试私有函数

+

测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数internal_adder的代码:

+
+Filename: src/lib.rs +
pub fn add_two(a: i32) -> i32 {
+    internal_adder(a, 2)
+}
+
+fn internal_adder(a: i32, b: i32) -> i32 {
+    a + b
+}
+
+#[cfg(test)]
+mod tests {
+    use internal_adder;
+
+    #[test]
+    fn internal() {
+        assert_eq!(4, internal_adder(2, 2));
+    }
+}
+
+
+

Listing 11-6: Testing a private function

+
+
+

因为测试也不过是 Rust 代码而tests也只是另一个模块,我们完全可以在一个测试中导入并调用internal_adder。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。

+

集成测试

diff --git a/src/ch11-01-writing-tests.md b/src/ch11-01-writing-tests.md index 82e94f7..e638de6 100644 --- a/src/ch11-01-writing-tests.md +++ b/src/ch11-01-writing-tests.md @@ -2,4 +2,235 @@ > [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-01-writing-tests.md) >
-> commit 77370c073661548dd56bbcb43cc64713585acbba \ No newline at end of file +> commit 77370c073661548dd56bbcb43cc64713585acbba + +测试是一种使用特定功能的 Rust 函数,它用来验证非测试的代码按照期望的方式运行。我们讨论过的任何 Rust 代码规则都适用于测试!让我们看看 Rust 提供的具体用来编写测试的功能:`test`属性、一些宏和`should_panic`属性。 + +### `test`属性 + +作为最简单例子,Rust 中的测试就是一个带有`test`属性注解的函数。让我们使用 Cargo 来创建一个新的库项目`adder`: + +``` +$ cargo new adder + Created library `adder` project +$ cd adder +``` + +Cargo 在创建新的库项目时自动生成一个简单的测试。这是`src/lib.rs`中的内容: + +Filename: src/lib.rs + +```rust +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + } +} +``` + +现在让我们暂时忽略`tests`模块和`#[cfg(test)]`注解并只关注函数。注意它之前的`#[test]`:这个属性表明这是一个测试函数。这个函数目前没有任何内容,所以绝对是可以通过的!使用`cargo test`来运行测试: + +``` +$ 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 ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured + + Doc-tests adder + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured +``` + +Cargo 编译并运行了测试。这里有两部分输出:本章我们将关注第一部分。第二部分是文档测试的输出,第十四章会介绍他们。现在注意看这一行: + +```text +test it_works ... ok +``` + +`it_works`文本来源于测试函数的名称。 + +这里也有一行总结告诉我们所有测试的聚合结果: + +``` +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +``` + +### `assert!`宏 + +空的测试函数之所以能通过是因为任何没有`panic!`的测试都是通过的,而任何`panic!`的测试都算是失败。让我们使用`assert!宏来使测试失败: + +Filename: src/lib.rs + +```rust +#[test] +fn it_works() { + assert!(false); +} +``` +`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 + +failures: + +---- it_works stdout ---- + thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5 +note: Run with `RUST_BACKTRACE=1` for a backtrace. + + +failures: + it_works + +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`值。 + +下面是分别使用这两个宏其会测试通过的例子: + +Filename: src/lib.rs + +``` +#[test] +fn it_works() { + assert_eq!("Hello", "Hello"); + + assert_ne!("Hello", "world"); +} +``` + +也可以对这些宏指定可选的第三个参数,它是一个会加入错误信息的自定义文本。这两个宏展开后的逻辑看起来像这样: + +```rust,ignore +// 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 + ) +} + +// 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 + ) +} +``` + +看看这个因为`hello`不等于`world`而失败的测试。我们还增加了一个自定义的错误信息,`greeting operation failed`: + +Filename: src/lib.rs + +```rust +#[test] +fn a_simple_case() { + let result = "hello"; // this value would come from running your code + assert_eq!(result, "world", "greeting operation failed"); +} +``` + +毫无疑问运行这个测试会失败,而错误信息解释了为什么测试失败了并且带有我们的指定的自定义错误信息: + +```text +---- 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 +``` + +`assert_eq!`的两个参数被称为 "left" 和 "right" ,而不是 "expected" 和 "actual" ;值的顺序和硬编码的值并没有什么影响。 + +因为这些宏使用了`==`和`!=`运算符并使用调试格式打印这些值,进行比较的值必须实现`PartialEq`和`Debug` 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 + +```rust +#[test] +#[should_panic] +fn slice_not_on_char_boundaries() { + let s = "Здравствуйте"; + &s[0..1]; +} +``` + +
+ +Listing 11-1: A test expecting a `panic!` + +
+
+ +这个测试是成功的,因为我们表示代码应该会 panic。相反如果代码因为某种原因没有产生`panic!`则测试会失败。 + +使用`should_panic`的测试是脆弱的,因为难以保证测试不会因为一个不同于我们期望的原因失败。为了帮助解决这个问题,`should_panic`属性可以增加一个可选的`expected`参数。测试工具会确保错误信息里包含我们提供的文本。一个比列表 11-1 更健壮的版本如列表 11-2 所示: + +
+Filename: src/lib.rs + +```rust +#[test] +#[should_panic(expected = "do not lie on character boundary")] +fn slice_not_on_char_boundaries() { + let s = "Здравствуйте"; + &s[0..1]; +} +``` + + + +
+ +Listing 11-2: A test expecting a `panic!` with a particular message + +
+
+ +请自行尝试当`should_panic`的测试出现 panic 但并不符合期望的信息时会发生什么:在测试中因为不同原因造成`panic!`,或者将期望的 panic 信息改为并不与字母字节边界 panic 信息相匹配。 \ No newline at end of file diff --git a/src/ch11-02-running-tests.md b/src/ch11-02-running-tests.md index e69de29..085647a 100644 --- a/src/ch11-02-running-tests.md +++ b/src/ch11-02-running-tests.md @@ -0,0 +1,224 @@ +## 运行测试 + +> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-02-running-tests.md) +>
+> commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc + +类似于`cargo run`会编译代码并运行生成的二进制文件,`cargo test`在测试模式下编译代码并运行生成的测试二进制文件。`cargo test`生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。 + +可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给`cargo test`,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是`--`:`cargo test`之后列出了传递给`cargo test`的参数,接着是分隔符`--`,之后是传递给测试二进制文件的参数。 + +### 并行运行测试 + +测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。 + +如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递`--test-threads`参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作: + +``` +$ cargo test -- --test-threads=1 +``` + +### 捕获测试输出 + +Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了`println!`而测试通过了,你将不会在终端看到`println!`的输出。这个行为可以通过向测试二进制文件传递`--nocapture`参数来禁用: + +``` +$ cargo test -- --nocapture +``` + +### 通过名称来运行测试的子集 + +有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。`cargo test`有一个参数允许你通过指定名称来运行特定的测试。 + +列表 11-3 中创建了三个如下名称的测试: + +
+Filename: src/lib.rs + +```rust +#[test] +fn add_two_and_two() { + assert_eq!(4, 2 + 2); +} + +#[test] +fn add_three_and_two() { + assert_eq!(5, 3 + 2); +} + +#[test] +fn one_hundred() { + assert_eq!(102, 100 + 2); +} +``` + +
+ +Listing 11-3: Three tests with a variety of names + +
+
+ +使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试: + +``` +$ cargo test + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running target/debug/deps/adder-abcabcabc + +running 3 tests +test add_three_and_two ... ok +test one_hundred ... ok +test add_two_and_two ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured +``` + +可以传递任意测试的名称来只运行那个测试: + +``` +$ cargo test one_hundred + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running target/debug/deps/adder-abcabcabc + +running 1 test +test one_hundred ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +``` + +也可以传递名称的一部分,`cargo test`会运行所有匹配的测试: + +``` +$ cargo test add + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running target/debug/deps/adder-abcabcabc + +running 2 tests +test add_three_and_two ... ok +test add_two_and_two ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured +``` + +模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫`adding`的模块和一个叫`subtracting`的模块并分别带有测试,如列表 11-4 所示: + +
+Filename: src/lib.rs + +```rust +mod adding { + #[test] + fn add_two_and_two() { + assert_eq!(4, 2 + 2); + } + + #[test] + fn add_three_and_two() { + assert_eq!(5, 3 + 2); + } + + #[test] + fn one_hundred() { + assert_eq!(102, 100 + 2); + } +} + +mod subtracting { + #[test] + fn subtract_three_and_two() { + assert_eq!(1, 3 - 2); + } +} +``` + +
+ +Listing 11-4: Tests in two modules named `adding` and `subtracting` + +
+
+ +执行`cargo test`会运行所有的测试,而模块名会出现在输出的测试名中: + +``` +$ cargo test + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running target/debug/deps/adder-abcabcabc + +running 4 tests +test adding::add_two_and_two ... ok +test adding::add_three_and_two ... ok +test subtracting::subtract_three_and_two ... ok +test adding::one_hundred ... ok +``` + +运行`cargo test adding`将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试: + +``` +$ cargo test adding + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running target/debug/deps/adder-abcabcabc + +running 3 tests +test adding::add_three_and_two ... ok +test adding::one_hundred ... ok +test adding::add_two_and_two ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured +``` + +### 除非指定否则忽略某些测试 + +有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数`cargo test`命令,我们希望能排除它。无需为`cargo test`创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用`ignore`属性: + +Filename: src/lib.rs + +```rust +#[test] +fn it_works() { + assert!(true); +} + +#[test] +#[ignore] +fn expensive_test() { + // code that takes an hour to run +} +``` + +现在运行测试,将会发现`it_works`运行了,而`expensive_test`没有: + +``` +$ cargo test + Compiling adder v0.1.0 (file:///projects/adder) + Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs + Running target/debug/deps/adder-abcabcabc + +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 +``` + +我们可以通过`cargo test -- --ignored`来明确请求只运行那些耗时的测试: + +``` +$ cargo test -- --ignored + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running target/debug/deps/adder-abcabcabc + +running 1 test +test expensive_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +``` + +通过这种方式,大部分时间运行`cargo test`将是快速的。当需要检查`ignored`测试的结果而且你也有时间等待这个结果的话,可以选择执行`cargo test -- --ignored`。 \ No newline at end of file diff --git a/src/ch11-03-test-organization.md b/src/ch11-03-test-organization.md index e69de29..883c33c 100644 --- a/src/ch11-03-test-organization.md +++ b/src/ch11-03-test-organization.md @@ -0,0 +1,122 @@ +## 测试的组织结构 + +> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/src/ch11-03-test-organization.md) +>
+> commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc + +正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与**集成测试**(*unit tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。 + +### 单元测试 + +单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 *src* 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的`tests`模块中。 + +#### 测试模块和`cfg(test)` + +通过将测试放进他们自己的模块并对该模块使用`cfg`注解,我们可以告诉 Rust 只在执行`cargo test`时才编译和运行测试代码。这在当我们只希望用`cargo build`编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。 + +还记得上一部分新建的`adder`项目吗?Cargo 为我们生成了如下代码: + +Filename: src/lib.rs + +```rust +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + } +} +``` + +我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。 + +首先,这里有一个属性`cfg`。`cfg`属性让我们声明一些内容只在给定特定的**配置**(*configuration*)时才被包含进来。Rust 提供了`test`配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。 + +接下来,`tests`包含了所有测试函数,而我们的代码则位于`tests`模块之外。`tests`模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。 + +从本章到现在,我们一直在为`adder`项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 *src/lib.rs* 中,放入`add_two`函数和带有一个检验代码的测试的`tests`模块,如列表 11-5 所示: + +
+Filename: src/lib.rs + +```rust +pub fn add_two(a: i32) -> i32 { + a + 2 +} + +#[cfg(test)] +mod tests { + use add_two; + + #[test] + fn it_works() { + assert_eq!(4, add_two(2)); + } +} +``` + +
+ +Listing 11-5: Testing the function `add_two` in a child `tests` module + +
+
+ +注意除了测试函数之外,我们还在`tests`模块中添加了`use add_two;`。这将我们想要测试的代码引入到了内部的`tests`模块的作用域中,正如任何内部模块需要做的那样。如果现在使用`cargo test`运行测试,它会通过: + +``` +running 1 test +test tests::it_works ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +``` + +如果我们忘记将`add_two`函数引入作用域,将会得到一个 unresolved name 错误,因为`tests`模块并不知道任何关于`add_two`函数的信息: + +``` +error[E0425]: unresolved name `add_two` + --> src/lib.rs:9:23 + | +9 | assert_eq!(4, add_two(2)); + | ^^^^^^^ unresolved name +``` + +如果这个模块包含很多希望测试的代码,在测试中列出每一个`use`语句将是很烦人的。相反在测试子模块中使用`use super::*;`来一次将所有内容导入作用域中是很常见的。 + +#### 测试私有函数 + +测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数`internal_adder`的代码: + +
+Filename: src/lib.rs + +```rust +pub fn add_two(a: i32) -> i32 { + internal_adder(a, 2) +} + +fn internal_adder(a: i32, b: i32) -> i32 { + a + b +} + +#[cfg(test)] +mod tests { + use internal_adder; + + #[test] + fn internal() { + assert_eq!(4, internal_adder(2, 2)); + } +} +``` + +
+ +Listing 11-6: Testing a private function + +
+
+ +因为测试也不过是 Rust 代码而`tests`也只是另一个模块,我们完全可以在一个测试中导入并调用`internal_adder`。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。 + +### 集成测试 +