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 之间可传递(json = ["serde/json"]) – profiles:[profile.dev] 与 [profile.release] 控制优化等级、LTO 等,同一份代码两种构建档位

使用场景: – CLI + 核心库 + 服务器拆成三个 crate(本模块 projects 路线即此形态) – 默认精简、按需开启重依赖(如 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,基线见模块 README

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

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

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

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

[features]
default = []
json = ["serde/json"]     # feature 可传递开启依赖的 feature

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

示例四:workspace 布局

# 根 Cargo.toml —— workspace 成员共享 Cargo.lock 与 target/
[workspace]
resolver = "2"
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 divide             # 按名称过滤
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 – 依赖版本查证(基线核实来源)

练习与实践

练习一:测试驱动的 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")]。


知识图谱

前置知识

graph TD
    A[Cargo 基础命令] --> B[模块系统与可见性]
    B --> C[Cargo 工程化与单元测试]

后续学习

graph LR
    C[Cargo 工程化与单元测试] --> D[Criterion 基准测试]
    C --> E[GitHub Actions CI]

文档交叉引用

相关文档

  • 📄 集合与迭代器: 示例一使用的迭代器聚合
  • 📄 智能指针: 测试 should_panic 验证的 RefCell 行为
  • 📄 并发与 async: workspace 三 crate 的 server 成员走向
  • 📄 环境搭建: cargo 安装与工具链管理

参考章节

  • 📖 模块 README: 技术基线与模块路径图
  • 🛠️ 单元与集成测试(指南): 测试工程深入篇

总结

核心要点回顾

  1. Cargo 一体化消除构建碎片化: lock 锁版本、check 快反馈、test 跑三层
  2. workspace + features 支撑规模化: 版本一处声明,能力按需编译
  3. 测试金字塔内建于语言: 单元测私有、集成测契约、doctest 保示例——cargo test 一键全跑

学习成果检查

  • [ ] 能说清 [dependencies] 与 [dev-dependencies] 的边界?
  • [ ] 能写出 assert / should_panic / Result 三种测试?
  • [ ] 能搭起带 [workspace.dependencies] 的三成员 workspace?
  • [ ] 能解释 doctest 为什么让文档示例”永不腐烂”?

贡献与反馈

发现本文档有改进空间?欢迎在 Issues 报告问题、提出改进建议,或直接提交 PR 完善内容。


文档版本: v1.0.0最后更新: 2026年9月维护团队: Hello Team


💡 学习建议: 本篇是 basics 的收官——回到 模块 README 查看三路径视图,按自己的目标选择 frameworks / projects 路线继续。

🎯 下一步: 基础已备,进入实战:从 projects/ 的 CLI 工具项目开始,或跳转 frameworks/ 进入 Tauri 2 旗舰线。

暂无评论

发送评论 编辑评论

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