测试机构

正如本章开头提到的,测试是一门复杂的学科,并且 不同的人使用不同的术语和组织。Rust 社区 从两个主要类别来考虑测试:单元测试和集成 测试。单元测试较小且更集中,孤立地测试一个模块 ,并且可以测试私有接口。集成测试完全是 external 添加到您的库中,并以与任何其他 external 相同的方式使用您的代码 代码会,仅使用公共接口并可能执行多个 modules per test 的 modules 进行验证。

编写这两种测试都很重要,以确保您的 库正在做你期望他们单独和一起做的事情。

单元测试

单元测试的目的是在独立于 其余代码,以快速确定代码在哪里工作,在哪里不起作用 预期。您将单元测试放在每个文件的 src 目录中,其中 代码。约定是在每个文件中创建一个命名的模块,以包含测试函数,并使用 .testscfg(test)

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

模块上的注解告诉 Rust 编译并 仅在运行时运行测试代码,而不是在运行时运行。当您只想构建库和 节省生成的 Compiled 工件中的空间,因为测试不是 包括。你会看到这一点,因为集成测试在不同的 目录中,则不需要注解。但是,由于 unit 测试与代码位于相同的文件中,您将用于指定 它们不应该包含在编译结果中。#[cfg(test)]testscargo testcargo build#[cfg(test)]#[cfg(test)]

回想一下,当我们在 本章,Cargo 为我们生成了这段代码:adder

文件名: src/lib.rs

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

在自动生成的模块上,该属性代表配置,并告诉 Rust 应该只包含以下项目 给定某个配置选项。在这种情况下,配置选项是 ,它由 Rust 提供,用于编译和运行测试。通过使用 attribute,Cargo 只有在我们主动运行测试时才会编译我们的测试代码 跟。这包括此 模块,以及带有 .testscfgtestcfgcargo test#[test]

测试私有函数

测试社区内部存在关于是否私有的争论 函数应该直接测试,其他语言会使其变得困难或 无法测试私有函数。无论您采用哪种测试意识形态 遵守,Rust 的隐私规则确实允许您测试私有函数。 考虑示例 11-12 中带有 private 函数 的代码 。internal_adder

文件名: src/lib.rs
pub fn add_two(a: usize) -> usize {
    internal_adder(a, 2)
}

fn internal_adder(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}
示例 11-12:测试私有函数

请注意,该函数未标记为 .测试只是 Rust 代码,而 module 只是另一个模块。正如我们在 在“引用模块树中项的路径”部分,子模块中的项可以使用其祖先模块中的项。在 在这个测试中,我们使用 将模块的父项的所有 项引入范围,然后测试可以调用 。如果你不认为 私有函数应该被测试,Rust 中没有任何东西可以强迫 你这样做。internal_adderpubteststestsuse super::*internal_adder

集成测试

在 Rust 中,集成测试完全在你的库之外。他们使用您的 库,这意味着它们只能调用 函数,这些函数是库的公共 API 的一部分。他们的目的是测试 库的许多部分是否能够正确地协同工作。代码单元 在集成时,它们本身正常工作可能会出现问题,因此请测试 集成代码的覆盖率也很重要。创建集成 tests,您首先需要一个 tests 目录。

tests 目录

我们在项目目录的顶层创建一个 tests 目录,接下来 到 src。Cargo 知道要在这个目录中查找集成测试文件。我们 然后,我们可以根据需要创建任意数量的测试文件,并且 Cargo 将编译每个 文件作为单独的 crate 进行。

让我们创建一个集成测试。示例 11-12 中的代码仍在 src/lib.rs 文件中,创建一个 tests 目录,并创建一个名为 tests/integration_test.rs 的新文件。您的目录结构应如下所示:

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

将示例 11-13 中的代码输入到 tests/integration_test.rs 文件中。

文件名: tests/integration_test.rs
use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}
示例 11-13:crate 中函数的集成测试adder

tests 目录中的每个文件都是一个单独的 crate,因此我们需要将 库添加到每个 Test Crate 的 scope 中。因此,我们在代码的顶部添加了一些代码,这在单元测试中是不需要的。use adder::add_two;

我们不需要在 tests/integration_test.rs 中使用 .Cargo 专门处理 tests 目录并编译文件 仅当我们运行 .立即运行:#[cfg(test)]cargo testcargo test

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)

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

输出的三个部分包括单元测试、集成测试和 文档测试。请注意,如果某个部分中的任何测试失败,则以下部分 不会运行。例如,如果单元测试失败,则不会有任何输出 进行集成和 doc 测试,因为这些测试仅在所有 unit 测试正在通过。

单元测试的第一部分与我们之前看到的相同:一行 对于每个单元测试(我们在示例 11-12 中添加的 name)和 然后是单元测试的摘要行。internal

集成测试部分以行 .接下来,每个测试函数都有一行 该集成测试和集成结果的摘要行 test 的 TEST 中。Running tests/integration_test.rsDoc-tests adder

每个集成测试文件都有自己的部分,因此如果我们在 tests 目录中添加更多文件,就会有更多的集成测试部分。

我们仍然可以通过指定 test 来运行特定的集成测试函数 函数的名称作为 .要在 特定的集成测试文件,请使用 的参数 ,后跟文件名:cargo test--testcargo test

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

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

此命令仅运行 tests/integration_test.rs 文件中的测试。

集成测试中的子模块

随着您添加更多集成测试,您可能希望在 tests 目录中创建更多文件以帮助组织它们;例如,您可以对测试 功能。如前所述,每个文件 在 tests 目录中编译为它自己的单独 crate,这很有用 用于创建单独的范围以更紧密地模拟最终用户的方式 使用你的 crate。但是,这意味着 tests 目录中的文件不会 与 src do 中的文件共享相同的行为,如您在第 7 章中学到的那样 关于如何将代码分成模块和文件。

当您 具有一组辅助函数,可在多个集成测试文件中使用,并且 您尝试按照 “将模块分成不同的 Files“部分添加到 将它们提取到一个通用模块中。例如,如果我们创建 tests/common.rs 并在其中放置一个名为 的函数,我们可以向其添加一些代码 我们想从多个测试文件中的多个测试函数调用:setupsetup

文件名: tests/common.rs

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

当我们再次运行测试时,我们将在 common.rs 文件的测试输出中看到一个新部分,即使该文件不包含任何测试函数或 我们是否从任何地方调用了该函数:setup

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

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

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

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

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

在测试结果中出现并显示 这不是我们想要的。我们只是想与其他 集成测试文件。为避免出现在测试输出中, 我们将创建 tests/common/mod.rs,而不是创建 tests/common.rs。这 project 目录现在如下所示:commonrunning 0 testscommon

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

这是 Rust 也理解的旧命名约定,我们 在 的 “Alternate File Paths” 部分中提到 第 7 章.以这种方式命名文件告诉 Rust 不要将模块 作为集成测试文件。当我们将函数代码移动到 tests/common/mod.rs 并删除 tests/common.rs 文件时, test output 将不再显示。tests 目录的子目录中的文件不会编译为单独的 crate,也不会在 test 中包含部分 输出。commonsetup

在我们创建了 tests/common/mod.rs 之后,我们可以从任何 集成测试文件作为模块。以下是从 tests/integration_test.rs 中的测试调用函数的示例:setupit_adds_two

文件名: tests/integration_test.rs

use adder::add_two;

mod common;

#[test]
fn it_adds_two() {
    common::setup();

    let result = add_two(2);
    assert_eq!(result, 4);
}

请注意,声明与 module 声明相同 我们在示例 7-21 中进行了演示。然后,在 test 函数中,我们可以调用该函数。mod common;common::setup()

Binary Crate 的集成测试

如果我们的项目是一个二进制 crate,它只包含一个 src/main.rs 文件,并且 没有 src/lib.rs 文件,我们不能在 tests 目录中创建集成测试,也无法将 src/main.rs 文件中定义的函数引入 范围。只有库 crate 公开其他 板条箱可以使用;binary crate 应该独立运行。use

这就是提供二进制文件的 Rust 项目具有 简单的 src/main.rs 文件,它调用位于 src/lib.rs 文件中的 logic。使用该结构,集成测试可以测试 library crate 一起使用,以使重要的功能可用。如果 重要功能有效,src/main.rs 文件中的少量代码也可以正常工作,并且少量代码不需要测试。use

总结

Rust 的测试功能提供了一种方法来指定代码应该如何运作 确保它继续按预期工作,即使您进行更改也是如此。单元测试 单独练习库的不同部分,并可以测试私有 实现细节。集成测试检查库的许多部分 正确地协同工作,并且它们使用库的公共 API 来测试代码 外部代码将以同样的方式使用它。尽管 Rust 的类型系统和 所有权规则有助于防止某些类型的错误,测试仍然很重要 减少与代码预期行为方式相关的 Logic Bug。

让我们将您在本章和之前学到的知识结合起来 章节进行项目工作!

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准