rust 快速入门——22 自动化测试

138 阅读15分钟

[!|center] 普若哥们儿

github.com/wu-hongbing…

gitee.com/wuhongbing/…

自动化测试

Rust 的所有权和类型系统在保证程序正确上非常有用,比如发生函数调用时Rust 会进行所有目前我们已经见过的类型检查和借用检查,这些检查会确保我们不会传递错误的类型或无效的引用给这个函数。不过这不可能捕获所有问题,比如函数的算法或逻辑是否符合预期,为此,Rust 包含了编写自动化软件测试的功能支持。

[!attention] Rust 编译器能够完成其它语言许多需要测试的方面,尽可能只需要开发者测试逻辑问题。

如何编写测试

Rust 中的测试函数是用来验证非测试代码是否是按照期望的方式运行的,测试函数体通常执行如下三种操作:

  1. 设置任何所需的数据或状态
  2. 运行需要测试的代码
  3. 断言其结果是我们所期望的

测试函数

作为最简单例子,Rust 中的测试就是一个带有 test 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据;结构体中用到的 derive 属性就是一个例子。为了将一个函数变成测试函数,需要在 fn 行之前加上 #[test]。当使用 cargo test 命令运行测试时,Rust 会构建一个测试执行程序用来调用被标注的函数,并报告每一个测试是通过还是失败。

每次使用 Cargo 新建一个库项目时,会自动为生成一个测试模块和一个测试函数。这个模块提供了一个编写测试的模板,这样每次开始新项目时不必去查找测试函数的具体结构和语法了。

先通过自动生成的测试模版来探索测试是如何工作的。下面创建一个新的库项目 adder,它会将两个数字相加:

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

adder 库中 src/lib.rs 的内容如下所示:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

暂时忽略 tests 模块和 #[cfg(test)] 注解并只关注函数本身。fn it_works() 之前的 #[test]属性表明这是一个测试函数。tests 模块中也可以有非测试的函数来帮助我们建立通用场景或进行常见操作,因此必须标明哪些函数是测试。

示例函数体通过使用 assert_eq! 宏来断言 2 加 2 等于 4。这是一个典型的测试的格式。

cargo test 命令会运行项目中所有的测试,如下例所示:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo 编译并运行了测试。可以看到第 6 行: running 1 test 表示运行了 1 个测试。第 7 行显示了生成的测试函数的名称 it_works,以及测试的运行结果 ok

第 9 行显示全体测试运行结果的摘要:test result: ok. 意味着所有测试都通过了,1 passed; 0 failed 表示通过或失败的测试数量。可以将一个测试标记为“忽略”,这样在特定情况下它就不会运行。因为之前我们并没有将任何测试标记为忽略,所以摘要中会显示 0 ignored。我们也没有过滤需要运行的测试,所以摘要中会显示 0 filtered out0 measured 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。

第 11 行测试输出中的以 Doc-tests adder 开头的这一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例,这个功能帮助我们使文档和代码保持同步!

让我们开始自定义测试来满足我们的需求。首先给 it_works 函数起个不同的名字,比如 exploration,像这样:

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }
}

并再次运行 cargo test。现在输出中将出现 exploration 而不是 it_works

现在让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。最简单的造成 panic 的方法是调用 panic! 宏。写入新测试 another 后, src/lib.rs 现在看起来如下例所示:

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

再次 cargo test 运行测试。输出表明 exploration 测试通过了而 another 失败了:

$ cargo test
   ...

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

第 5 行 test tests::anotherFAILED 而不是 ok 了。在单独测试结果和摘要之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,我们看到 another 因为在 src/lib.rs 的第 10 行 panicked at 'Make this test fail' 而失败的详细信息。下一部分列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助,我们可以通过使用失败测试的名称来只运行这个测试,以便调试。

最后是摘要行:总结本次测试,测试结果是 FAILED,有一个测试通过和一个测试失败。

使用 assert! 宏来检查结果

assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。需要向 assert! 宏提供一个求值为布尔值的参数。如果值是 trueassert! 什么也不做,同时测试会通过。如果值为 falseassert! 调用 panic! 宏,这会导致测试失败。assert! 宏帮助我们检查代码是否以期望的方式运行。

下例中有一个 Rectangle 结构体和一个 can_hold 方法,将它们放进 src/lib.rs 并使用 assert! 宏编写一些测试。

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
}

注意在 tests 模块中新增加了一行:use super::*;tests 是一个普通的模块,它遵循可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用 glob 全局导入,以便在 tests 模块中使用所有在外部模块定义的内容。

我们将测试命名为 larger_can_hold_smaller,并创建所需的两个 Rectangle 实例。接着调用 assert! 宏并传递 larger.can_hold(&smaller) 调用的结果作为参数。这个表达式预期会返回 true,所以测试应该通过。

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

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

下例编写一个对其参数加二并返回结果的函数 add_two。接着使用 assert_eq! 宏测试这个函数。

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

Rust 中,断言两个值相等的函数的参数被称为 leftright,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成 assert_eq!(add_two(2), 4),这时失败信息仍同样是 assertion failed: `(left == right)`

assert_ne! 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 是什么,不过能确定值绝对 不会 是什么的时候,这个宏最有用处。

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

自定义失败信息

可以向 assert!assert_eq!assert_ne! 宏传递一个可选的失败信息参数,在测试失败时将自定义失败信息一同打印出来。任何在 assert!assert_eq!assert_ne! 的必需参数之后指定的参数都会传递给 format! 宏,所以可以传递一个包含 {} 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义,当测试失败时就能更好的理解代码出了什么问题。

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

#[cfg(test)]
mod tests {
    use super::*;

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

运行测试将会看到更有价值的信息:

...
failures:

---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
...

使用 should_panic 检查 panic

should_panic 属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。

下例展示了一个检查 Guess::new 是否按照我们的期望出错的测试:

pub struct Guess {
    value: i32,
}

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

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

#[should_panic] 属性位于 #[test] 之后,对应的测试函数之前。测试通过时输出:

...
running 1 test
test tests::greater_than_100 - should panic ... ok

...

然而 should_panic 测试结果可能会非常含糊不清。should_panic 甚至在一些不是我们期望的原因而导致 panic 时也会通过。为了使 should_panic 测试结果更精确,可以给 should_panic 属性增加一个可选的 expected 参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑下例修改过的 Guess,这里 new 函数根据其值是过大还或者过小而提供不同的 panic 信息:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> 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 }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

这个测试会通过,因为 should_panic 属性中 expected 参数提供的值是 Guess::new 函数 panic 信息的子串。我们可以指定期望的整个 panic 信息,比如: Guess value must be less than or equal to 100, got 200.expected 信息的选择取决于 panic 信息有多独特或动态,和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在 else if value > 100 的情况下运行。

Result<T, E> 用于测试

目前为止,我们编写的测试在失败时都会 panic。我们也可以使用 Result<T, E> 编写测试!使用 Result<T, E> 重写,并在失败时返回 Err 而非 panic:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(());
        } else {
            Err(String::from("two plus two does not equal four"));
        }
    }
}

现在 it_works 函数的返回值类型为 Result<(), String>。在函数体中,不同于调用 assert_eq! 宏,而是在测试通过时返回 Ok(()),在测试失败时返回带有 StringErr

不能对使用 Result<T, E> 的测试使用 #[should_panic] 注解。为了断言一个操作返回 Err不要Result<T, E> 值使用问号表达式(?),而是使用 assert!(value.is_err())

控制测试如何运行

就像 cargo run 会编译代码并运行生成的二进制文件一样,cargo test 在测试模式下编译代码并运行生成的测试二进制文件。cargo test 产生的二进制文件的默认行为是并发运行所有的测试,并截获测试运行过程中产生的输出,阻止它们被显示出来,使得阅读测试结果相关的内容变得更容易。不过可以指定命令行参数来改变 cargo test 的默认行为。

可以将一部分命令行参数传递给 cargo test,而将另外一部分传递给生成的测试二进制文件。为了分隔这两种参数,需要先列出传递给 cargo test 的参数,接着是分隔符 --,再之后是传递给测试二进制文件的参数。运行 cargo test --help 会提示 cargo test 的有关参数,而运行 cargo test -- --help 可以提示在分隔符之后使用的有关参数。

并行或连续的运行测试

当运行多个测试时,Rust 默认使用线程来并行运行。这意味着测试会更快地运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,需要确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。

如果不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递 --test-threads 参数和希望使用线程的数量给测试二进制文件。例如:

cargo test -- --test-threads=1

这里将测试线程设置为 1,告诉程序不要使用任何并行机制。

显示函数输出

默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容。比如在测试中调用了 println! 而测试通过了,我们将不会在终端看到 println! 的输出,只会看到说明测试通过的提示行。如果测试失败了,则会看到所有标准输出和其他错误信息。

如果希望看到通过的测试中打印的值,也可以在结尾加上 --show-output 告诉 Rust 显示成功测试的输出。

cargo test -- --show-output

通过指定名字来运行部分测试

有时运行整个测试集会耗费很长时间。如果你只负责特定位置的代码,可能会希望只运行与这些代码相关的测试。可以向 cargo test 传递所希望运行的测试名称的参数来选择运行哪些测试。

下例为 add_two 函数创建了三个测试:

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_two_and_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn add_three_and_two() {
        assert_eq!(5, add_two(3));
    }

    #[test]
    fn one_hundred() {
        assert_eq!(102, add_two(100));
    }
}

可以选择具体运行哪一个:

cargo test add_two_and_two

注意,不能像这样在后面指定多个测试名称来测试多个测试项,只有传递给 cargo test 的第一个值才会被使用。

可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行。例如,因为头两个测试的名称包含 add,可以通过 cargo test add 来运行这两个测试:

$ cargo test add
...
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

这运行了所有名字中带有 add 的测试,也过滤掉了名为 one_hundred 的测试。注意,测试所在的模块也是测试名称的一部分,所以可以通过模块名来运行一个模块中的所有测试。

忽略某些测试

有时一些特定的测试执行起来是非常耗费时间的,所以希望在测试时能排除它们。这可以通过参数列举出所有希望运行的测试来做到,也可以使用 ignore 属性来标记耗时的测试并排除它们,如下所示:

#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
    // 需要运行一个小时的代码
}

对于想要排除的测试,我们在 #[test] 之后增加了 #[ignore] 行。现在如果运行测试,就会发现 it_works 运行了,而 expensive_test 没有运行。

如果只希望运行被忽略的测试,可以使用 cargo test -- --ignored。如果希望不管是否忽略都要运行全部测试,可以运行 cargo test -- --include-ignored

测试的组织结构

测试分为单元测试unit tests)与 集成测试integration tests)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的,完全以用户的身份使用使用你的代码。

单元测试

单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确地验证某个单元的代码功能是否符合预期。单元测试与它们要测试的代码共同存放在位于 src 目录下相同的文件中。规范是在每个文件内部创建包含测试函数的 tests 模块,并使用 cfg(test) 标注模块。

测试模块和 #[cfg(test)]

测试模块的 #[cfg(test)] 注解告诉 Rust 只在执行 cargo test 时才编译和运行测试代码,而在运行 cargo build 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 #[cfg(test)] 注解

回忆本章之前新建的 adder 项目,Cargo 为我们生成了如下代码:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

上述代码就是自动生成的测试模块。cfg 属性代表配置configuration) ,它告诉 Rust,接下来的项,只有在给定特定配置选项时,才会被包含。在这种情况下,配置选项是 test,即 Rust 所提供的用于编译和运行测试的配置选项。通过使用 cfg 属性,Cargo 只会在我们主动使用 cargo test 运行测试时才编译测试代码。这包括测试模块中可能需要的辅助函数,以及标注为 #[test] 的函数。

测试私有函数

测试社区中一直存在关于是否应该对私有函数直接进行测试的论战,而在其他语言中想要测试私有函数是一件困难的,甚至是不可能的事。不过无论你坚持哪种测试意识形态,Rust 的私有性规则允许测试私有函数。考虑示下例中带有私有函数 internal_adder 的代码:

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 super::*;

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

注意 internal_adder 函数并没有标记为 pub。测试也不过是 Rust 代码,同时 tests 也仅仅是另一个模块,子模块的项可以使用其上级模块的项。在测试中,我们通过 use super::*test 模块的父模块的所有项引入了作用域,接着测试调用了 internal_adder

集成测试

在 Rust 中,集成测试对于你需要测试的库来说完全是外部的,同其他使用库的代码一样使用库文件,也就是说它们只能调用一部分库中的公有 API。集成测试的目的是测试库的多个部分能否一起正常工作。一些单独能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,你需要先创建一个 tests 目录。

tests 目录

为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。

让我们来创建一个集成测试。保留上例中 src/lib.rs 的代码。创建一个 tests 目录,新建一个文件 tests/integration_test.rs。目录结构应该看起来像这样:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

tests/integration_test.rs 文件内容为:

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

因为每一个 tests 目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。为此与单元测试不同,我们需要在文件顶部添加 use adder

并不需要将 tests/integration_test.rs 中的任何代码标注为 #[cfg(test)]tests 文件夹在 Cargo 中是一个特殊的文件夹,Cargo 只会在运行 cargo test 时编译这个目录中的文件。

运行 cargo test

$ cargo test
...

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

现在有了三个部分的输出:单元测试、集成测试和文档测试。注意如果一个部分的任何测试失败,之后的部分都不会运行。例如如果一个单元测试失败,则不会有任何集成测试和文档测试的输出,因为这些测试只会在所有单元测试都通过后才会执行。

第一部分单元测试与我们之前见过的一样:每个单元测试一行,接着是一个单元测试的摘要行。

集成测试部分以行 Running tests/integration_test.rs 开头。接下来每一行是一个集成测试中的测试函数,以及一个位于 Doc-tests adder 部分之前的集成测试的摘要行。

每一个集成测试文件有对应的测试结果部分,所以如果在 tests 目录中增加更多文件,测试结果中就会有更多集成测试结果部分。

我们仍然可以通过指定测试函数的名称作为 cargo test 的参数来运行特定集成测试。也可以使用 cargo test--test 后跟文件的名称来运行某个特定集成测试文件中的所有测试:

$ cargo test --test integration_test

这个命令只运行了 tests 目录中我们指定的文件 integration_test.rs 中的测试。

集成测试中的子模块

随着集成测试的增加,你可能希望在 tests 目录创建更多文件以便更好地组织它们,例如根据测试的功能来将测试分组。如前所述,tests 目录中的每一个文件都被编译成一个单独的 crate,这有助于创建独立的作用域,以便更接近于最终用户使用你的 crate 的方式。但这意味着,tests 目录中的文件的行为,和你在之前学习如何将代码分为模块和文件时,学到的 src 中的文件的行为不一样。

当你有一些在多个集成测试文件都会用到的辅助函数,而你尝试将它们提取到一个通用的模块中时, tests 目录中文件行为的不同就会凸显出来。例如,如果我们可以创建一个 tests/common.rs 文件并创建一个名叫 setup 的函数,我们希望这个函数能被多个测试文件的测试函数调用:

pub fn setup() {
    // setup code specific to your library's tests would go here
}

如果再次运行测试,将会在测试结果中看到一个新的对应 common.rs 文件的测试结果部分,即便这个文件并没有包含任何测试函数,也没有任何地方调用了 setup 函数:

$ cargo test
...
running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

...
     Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok
...

我们并不想要 common 出现在测试结果中显示 running 0 tests 。我们只是希望其能被其他多个集成测试文件中调用罢了。为了不让 common 出现在测试输出中,我们将创建 tests/common/mod.rs ,而不是创建 tests/common.rs 。现在项目目录结构看起来像这样:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

这是一种老的命名规范,这样命名告诉 Rust 不要将 common 看作一个集成测试文件。将 setup 函数代码移动到 tests/common/mod.rs 并删除 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中。

一旦拥有了 tests/common/mod.rs ,就可以将其作为模块以便在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用 setup 函数的 it_adds_two 测试的例子:

use adder;

mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

注意 mod common; 声明。接着在测试函数中就可以调用 common::setup() 了。

二进制 crate 的集成测试

如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs ,这样就不可能在 tests 目录创建集成测试并使用 extern crate 导入 src/main.rs 中定义的函数。只有库 crate 才会向其他 crate 暴露了可供调用和使用的函数,而二进制 crate 只是为了单独运行。

这就是许多 Rust 二进制项目使用一个简单的 src/main.rs 调用 src/lib.rs 中的逻辑的原因之一。因为通过这种结构,集成测试 就可以 通过 extern crate 测试库 crate 中的主要功能了,而如果这些重要的功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。