Cargo 与测试

要点

Cargo 把源码、依赖、测试与构建配置组织起来。单测靠近内部逻辑,集成测试从公开 API 使用 crate,两者观察边界不同。


核心概念

概念一:Cargo 是构建器 + 包管理器 + 测试运行器

定义: 一个 Cargo.toml 声明元数据与依赖,Cargo 负责解析依赖图、增量编译、跑测试、生成文档——Rust 生态没有”构建脚本碎片化”问题,因为只有一个 Cargo。

关键特性:

  • [dependencies]:运行时依赖;[dev-dependencies]:仅测试/示例/基准可用,不进发布产物
  • Cargo.lock:锁定精确版本,应用项目应提交入库,保证团队与 CI 构建一致
  • cargo check:只做类型检查不生成代码,是最快的反馈回路

使用场景:

  • 任何 Rust 项目从 cargo new 开始,团队以 Cargo.lock 保证构建一致

概念二:workspace 与 features 是”规模化的两把钥匙”

定义: workspace 把多个 crate 组成一个仓库统一管理(共享 lock 文件与 target 目录);features 让同一个 crate 按需裁剪功能(条件编译)。

关键特性:

  • workspace:根 Cargo.toml 列 members,子 crate 通过 path 互相依赖;[workspace.dependencies] 统一版本,避免依赖漂移
  • features:#[cfg(feature = "json")] 门控代码;feature 可传递开启依赖的 feature 或可选依赖(写法见示例三)
  • profiles:[profile.dev] 与 [profile.release] 控制优化等级、LTO 等,同一份代码两种构建档位

使用场景:

  • CLI + 核心库 + 服务器拆成三个 crate(中大型项目常用此形态)
  • 默认精简、按需开启重依赖(如 TLS、序列化)

概念三:测试金字塔的三层

定义: Rust 内建三层测试——单元测试(与代码同文件)、集成测试(tests/ 独立 crate)、文档测试(doc comment 里的代码块),cargo test 一次全跑。

关键特性:

  • 单元测试:#[cfg(test)] mod tests,可测私有函数,编译进测试二进制、不进发布产物
  • 集成测试:tests/*.rs 每个文件是独立 crate,只能走公共 API——测的是”用户视角”
  • doctest:文档里的示例代码会被提取编译运行——示例即测试,文档永不腐烂的关键机制

使用场景:

  • 逻辑分支多 → 单元测试铺密度
  • 公共函数带示例 → doctest 一石二鸟

实践指南

步骤一:为已有函数补测试

目标: 掌握 #[cfg(test)] 三种断言形态。

操作指南:

  1. 在源文件底部加 #[cfg(test)] mod tests { use super::*; }
  2. #[test] 标记用例;普通断言 assert_eq!、预期 panic 用 #[should_panic]、可失败场景返回 Result
  3. cargo test 运行,cargo test <名称片段> 过滤

验证方法: 示例一 4 个用例全部 ok。

步骤二:给公共 API 配 doctest

目标: 让文档示例成为可执行契约。

操作指南:

  1. 在 /// 文档注释里写 ``` 包裹的代码块
  2. doctest 中直接使用 crate 名访问公共 API(rustdoc 自动包 fn main)
  3. cargo test 时 doctest 单独计一组

验证方法: 示例二的 doctest 在 cargo test 输出中显示为 myapp::add 用例。

步骤三:拆 workspace

目标: 多 crate 仓库的标准布局。

操作指南:

  1. 根目录 Cargo.toml 声明 [workspace] 与 members
  2. 子 crate 的依赖引用统一走 [workspace.dependencies] + workspace = true
  3. 根目录执行 cargo test / cargo check 会作用于全部成员

验证方法: cargo build 一次产出多个 crate,target/ 只有一份。

步骤四:调 profiles 与 features

目标: 开发快编译、发布高性能。

操作指南:

  1. [profile.release] 开 LTO 与 strip,对比 cargo build --release 前后产物体积
  2. 定义 features,用 cargo run --features json 切换功能面
  3. CI 里固定 --locked 使用 lock 文件

验证方法: 未开启 feature 时门控代码不参与编译(cargo build 无该符号)。


代码示例

示例一:单元测试三形态(assert / should_panic / Result)

// 块1:单元测试三形态(assert / should_panic / Result)
pub struct Stats {
    pub values: Vec<f64>,
}

impl Stats {
    pub fn mean(&self) -> Option<f64> {
        if self.values.is_empty() {
            return None;
        }
        Some(self.values.iter().sum::<f64>() / self.values.len() as f64)
    }

    // 约定"调用方保证非空":违反约定属于程序性 bug,用 panic 表达
    pub fn first(&self) -> f64 {
        self.values[0]
    }
}

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

    #[test]
    fn 空序列均值返回_none() {
        assert_eq!(Stats { values: vec![] }.mean(), None);
    }

    #[test]
    fn 均值计算正确() {
        let s = Stats { values: vec![1.0, 2.0, 3.0] };
        assert!((s.mean().unwrap() - 2.0).abs() < 1e-9);
    }

    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn 空集合取首元素应_panic() {
        let s = Stats { values: vec![] };
        let _ = s.first();
    }

    #[test]
    fn 结果风格测试() -> Result<(), String> {
        let s = Stats { values: vec![1.0, 2.0, 3.0] };
        let m = s.mean().ok_or("均值不可用")?;
        assert!((m - 2.0).abs() < 1e-9);
        Ok(())
    }
}

关键点解析:

  • #[cfg(test)] 让测试代码只在 cargo test 时编译,发布产物零负担
  • 三种形态对应三种失败语义:值错误(assert)、程序 bug(should_panic)、环境/输入失败(Result)
  • 测试函数名可直接用中文,cargo test 按名过滤时同样有效

示例二:文档测试(doctest)

// 块2:文档测试(doctest)——示例即测试
/// 计算两数之和。
///
/// # 示例
///
/// ```
/// assert_eq!(myapp::add(2, 3), 5);
/// ```
pub fn add(a: i64, b: i64) -> i64 {
    a + b
}

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

    #[test]
    fn 单元测试走同一逻辑() {
        assert_eq!(add(-1, 1), 0);
    }
}

关键点解析:

  • doctest 由 rustdoc 提取为独立小程序编译运行——文档示例错了,cargo test 直接红
  • 不希望执行的示例(如 ignore/no_run 标注)可按需控制,但默认”示例必须能跑”

示例三:Cargo.toml 分区详解

# Cargo.toml(单个 crate)
[package]
name = "myapp"
version = "0.1.0"
edition = "2024"          # 当前稳定 edition

[dependencies]
serde = { version = "1.0", features = ["derive"], optional = true }

[dev-dependencies]        # 仅测试/示例/基准可用,不进发布产物
1. 例如:pretty_assertions = "1"(版本以 crates.io 当前为准)

[profile.dev]
opt-level = 0             # 开发构建:最快编译

[profile.release]
opt-level = 3
lto = true                # 链接期优化:运行更快、编译更慢

[features]
default = []
1. serde 没有 json feature:把 serde 声明为 optional 依赖,
1. 再用 dep:serde 让 json feature 自动开启这个依赖
json = ["dep:serde"]

关键点解析:

  • edition 是编译方言开关而非版本号——同一份依赖可各自选择 edition
  • profiles 只在”构建方”的 Cargo.toml 生效,作为库发布时不携带

示例四:workspace 布局

# 根 Cargo.toml —— workspace 成员共享 Cargo.lock 与 target/
[workspace]
resolver = "3"
members = [
    "crates/core",
    "crates/cli",
    "crates/server",
]

[workspace.dependencies]  # 统一管理依赖版本,子 crate 用 workspace = true 引用
tokio = { version = "1.53", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }

[workspace.package]      # 统一元数据,子 crate 继承
version = "0.1.0"
edition = "2024"
myapp/
├── Cargo.toml            # 根:仅 [workspace],无 [package]
├── Cargo.lock            # 全仓库唯一 lock
├── crates/
│   ├── core/             # 纯逻辑库:单元测试放 src/lib.rs
│   ├── cli/              # 命令行入口:依赖 core
│   └── server/           # 服务入口:依赖 core
└── tests/                # (可选)跨 crate 集成场景放这里或各 crate 的 tests/

关键点解析:

  • 子 crate 的 Cargo.toml 写 tokio = { workspace = true },版本只在根定义一处
  • workspace 不发布任何东西,发布的是各子 crate

示例五:集成测试的边界

// tests/api_test.rs —— 独立 crate,只能访问 myapp 的公共 API
use myapp::Stats;

#[test]
fn 公共_api_从外部视角可用() {
    let s = Stats { values: vec![1.0, 2.0, 4.0] };
    let mean = s.mean().expect("非空集合必有均值");
    assert!((mean - 7.0 / 3.0).abs() < 1e-9);
}

关键点解析:

  • 集成测试看不到私有项——这是特性不是限制,它强制”公共 API 自洽”
  • 每个集成测试文件是独立 crate,共享的辅助代码放 tests/common/mod.rs 再 mod common; 引入

示例六:高频命令速查

# 测试相关
cargo test                    # 运行全部测试(单元 + 集成 + doctest)
cargo test mean               # 按名称过滤(如 mean 相关用例)
cargo test -- --nocapture     # 显示 println! 输出

1. 构建与运行
cargo check                   # 只查类型不生成代码,最快反馈
cargo build --release         # 优化构建(profile.release)
cargo doc --open              # 生成并打开文档(doctest 源头)

1. 质量工具
cargo fmt                     # 格式化
cargo clippy                  # lint 检查

关键点解析:

  • -- 之后是传给测试二进制的参数(libtest 参数),之前是 cargo 自己的参数

最佳实践

锁文件固定本工程的依赖解析,工具链与目标环境也参与可复现构建。测试名说明输入条件与预期,公共 API 的可执行示例帮助同时维护用法和行为;不能为保持绿灯无理由忽略失败示例。

profile 管理优化与相关构建设置,其中某些设置会影响可观察行为;feature 用于能力选择,也不只限于依赖开关。声明支持的组合并在 CI 验证,避免承诺所有组合都可用却只测试默认值。


常见问题

Q1: 单元测试放哪?tests/ 还是源文件里?

A: 默认源文件内 #[cfg(test)] mod tests(可访问私有项);跨模块、走公共 API 的场景放 tests/*.rs。判断口诀:”测内部实现细节 → 单元测试;测别人怎么用我 → 集成测试”。

Q2: workspace 与多仓库(各自独立)怎么选?

A: 代码同生命周期、同发布节奏 → workspace 一体化;独立演进、跨团队 → 多仓库。workspace 的隐含代价是”牵一发动全身”(lock 文件共享),收益是一次原子提交改全部成员。


相关资源

延伸阅读

工具资源

  • 开发工具: cargo-nextest – 更快的测试运行器(进阶可选)
  • 在线平台: crates.io – 查证依赖的最新版本与 feature 列表

练习与实践

练习一:测试驱动的 Stats 扩展

目标: 巩固三层测试。

任务要求:

  1. 给示例一的 Stats 补 variance()(方差)方法,先写测试后写实现
  2. 空集合行为自定(panic 或 Result),并在测试名中说明
  3. 为 mean 补一个 doctest

评估标准: cargo test 全绿;方差实现未使用 unwrap 于可失败路径。

练习二:三 crate workspace

目标: 工程化布局实战。

挑战任务:

  • 把前九篇任一练习改造为 core + cli + server workspace
  • 在根 [workspace.dependencies] 管理全部第三方依赖
  • 加一个 json feature:开启时 core 提供 to_json()(用 serde)

提示: 子 crate 继承 [workspace.package] 的 edition;feature 门控写 #[cfg(feature = "json")]。


前置知识

  1. Cargo 基础命令
  2. 模块系统与可见性
  3. Cargo 工程化与单元测试

后续学习

  • Cargo 工程化与单元测试 → 基准测试与持续集成的接入方式(参考 Cargo 官方文档)
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇