要点
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)] 三种断言形态。
操作指南:
- 在源文件底部加
#[cfg(test)] mod tests { use super::*; } #[test]标记用例;普通断言assert_eq!、预期 panic 用#[should_panic]、可失败场景返回Resultcargo test运行,cargo test <名称片段>过滤
验证方法: 示例一 4 个用例全部 ok。
步骤二:给公共 API 配 doctest
目标: 让文档示例成为可执行契约。
操作指南:
- 在
///文档注释里写```包裹的代码块 - doctest 中直接使用 crate 名访问公共 API(rustdoc 自动包
fn main) cargo test时 doctest 单独计一组
验证方法: 示例二的 doctest 在 cargo test 输出中显示为 myapp::add 用例。
步骤三:拆 workspace
目标: 多 crate 仓库的标准布局。
操作指南:
- 根目录
Cargo.toml声明[workspace]与members - 子 crate 的依赖引用统一走
[workspace.dependencies]+workspace = true - 根目录执行
cargo test/cargo check会作用于全部成员
验证方法: cargo build 一次产出多个 crate,target/ 只有一份。
步骤四:调 profiles 与 features
目标: 开发快编译、发布高性能。
操作指南:
[profile.release]开 LTO 与strip,对比cargo build --release前后产物体积- 定义
features,用cargo run --features json切换功能面 - 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 文件共享),收益是一次原子提交改全部成员。
相关资源
延伸阅读
- 官方文档: The Rustbook ch.11 – Writing Automated Tests – 测试体系官方章节
- 官方文档: The Cargo Book – profiles / features / workspace 全量手册
工具资源
- 开发工具: cargo-nextest – 更快的测试运行器(进阶可选)
- 在线平台: crates.io – 查证依赖的最新版本与 feature 列表
练习与实践
练习一:测试驱动的 Stats 扩展
目标: 巩固三层测试。
任务要求:
- 给示例一的
Stats补variance()(方差)方法,先写测试后写实现 - 空集合行为自定(panic 或 Result),并在测试名中说明
- 为
mean补一个 doctest
评估标准: cargo test 全绿;方差实现未使用 unwrap 于可失败路径。
练习二:三 crate workspace
目标: 工程化布局实战。
挑战任务:
- 把前九篇任一练习改造为
core + cli + serverworkspace - 在根
[workspace.dependencies]管理全部第三方依赖 - 加一个
jsonfeature:开启时 core 提供to_json()(用 serde)
提示: 子 crate 继承 [workspace.package] 的 edition;feature 门控写 #[cfg(feature = "json")]。
前置知识
- Cargo 基础命令
- 模块系统与可见性
- Cargo 工程化与单元测试
后续学习
- Cargo 工程化与单元测试 → 基准测试与持续集成的接入方式(参考 Cargo 官方文档)