cratosw

测试

测试

测试组织

.NET 里通常使用独立测试项目承载测试代码(xUnit/NUnit/MSTest 等均如此), 因此测试代码会位于与被测代码不同的程序集。

Rust 更常见的约定是:单元测试放在被测模块同文件内的 tests 子模块中:

  • 被测代码与测试并排放置,阅读与维护更直接
  • 测试作为子模块可访问内部实现,无需类似 .NET [InternalsVisibleTo] 的配置

测试子模块一般使用 #[cfg(test)] 标注,只在 cargo test 时参与编译与执行。
具体测试函数使用 #[test] 标注。

集成测试通常放在与 src 同级的 tests 目录中。cargo test 会将该目录下每个文件 视为独立 crate 并执行其中所有 #[test]。因此该目录内模块通常不必再写 #[cfg(test)]

另见:

运行测试

dotnet test 在 Rust 中最接近的是 cargo test

cargo test 默认并行执行测试。如需串行执行:

cargo test -- --test-threads=1

更多说明见 “Running Tests in Parallel or Consecutively”。

测试输出

复杂集成测试或端到端测试常需要日志输出。
在 Rust 中可直接使用 println!,行为类似 NUnit 的 Console.WriteLine。 默认情况下 cargo test 会捕获输出;如需显示,可加 --show-output

cargo test --show-output

更多说明见 “Showing Function Output”。

断言

.NET 的断言 API 随框架不同而变化。以 xUnit 为例:

[Fact]
public void Something_Is_The_Right_Length()
{
    var value = "something";
    Assert.Equal(9, value.Length);
}

Rust 标准库已内置常用断言宏,大多数场景无需额外框架:

示例:

#[test]
fn something_is_the_right_length() {
    let value = "something";
    assert_eq!(9, value.len());
}

标准库本身不提供 xUnit [Theory] 这类数据驱动测试机制。

Mock(模拟)

.NET 常借助 Moq、NSubstitute 等框架模拟依赖。Rust 也有对应 crate,如 mockall
此外,Rust 还能利用 条件编译 + cfg attribute 做轻量 mock,而不依赖额外框架。

#[cfg(test)] 表示仅在 cargo test 时编译该代码(底层相当于 rustc --test)。
对应地,#[cfg(not(test))] 表示仅在非测试构建中包含该代码。

下面示例演示如何 mock 标准库函数 var_os,让 get_env 在测试构建与普通构建使用不同实现:

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

/// 读取环境变量并返回其值(若存在)。
/// 若值不是合法 Unicode,则 panic。
pub fn get_env(key: &str) -> Option<String> {
    #[cfg(not(test))]                 // 常规构建
    use std::env::var_os;             // 使用标准库实现
    #[cfg(test)]                      // 测试构建
    use tests::var_os_mock as var_os; // 使用 mock 实现

    let val = var_os(key);
    val.map(|s| s.to_str()
                 .unwrap()
                 .to_owned())
}

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

    pub(crate) fn var_os_mock(key: &str) -> Option<OsString> {
        match key {
            "FOO" => Some("BAR".into()),
            _ => None
        }
    }

    #[test]
    fn get_env_when_var_undefined_returns_none() {
        assert_eq!(None, get_env("???"));
    }

    #[test]
    fn get_env_when_var_defined_returns_some_value() {
        assert_eq!(Some("BAR".to_owned()), get_env("FOO"));
    }
}

代码覆盖率

.NET 在覆盖率分析上有较成熟工具链(Visual Studio 内置、VS Code 插件、coverlet 等)。

Rust 也提供了 内建覆盖率能力
在编辑器可视化方面,可结合 VS Code 的 Coverage GuttersTarpaulin(或其他可生成 LCOV 的工具)。

示例命令:

cargo tarpaulin --ignore-tests --out Lcov

命令会生成 LCOV 文件。开启 Coverage Gutters: Watch 后,可在编辑器里看到行级覆盖率标记。

注意:LCOV 文件位置很关键。若工程是多包 workspace(见项目结构)并在根目录生成了 --workspace 覆盖率文件,插件通常会优先读取它。实际排查单包时,建议在目标包目录单独生成。

On this page