← 返回模块
3.5.1.5beta 可读 · 未来付费校验通过内容版本 2026-05-27

模块、Cargo 项目与单元测试

3.5.1 · Rust 基础 · 编程

国内某私募中频组新加入的应届工程师周一第一天克隆了项目仓库。仓库的 Cargo.toml 声明一个 library crate 加一个 binary crate; src/lib.rs 暴露一个 pricing 模块; src/pricing.rs 装着 Lesson 2 写过的 510300.SH 沪深300 ETF 期权闭式 Black-Scholes call 定价; 顶层 tests/ 目录里有三条集成测试针对 library 的公开 API; CI 跑 cargo fmt --checkcargo clippy -- -D warningscargo test。这位新人的日常循环就是你接下来整个职业生涯都会敲的循环: 改一个文件、cargo checkcargo testcargo fmt、提交。本课收尾 Module 3.5.1, 把一个真实 Rust 项目 (而不是单 src/main.rs 文件) 的样子讲清——模块系统、带依赖的 Cargo manifest、单元测试与集成测试、日常质量工具链巡游。

模块系统

Rust 的模块是「命名空间与可见性」机制, 不是构建系统也不是文件布局——Cargo 管构建, 模块管「谁能在哪里看到谁」。内联模块: mod name { ... items ... }。文件型模块: 父位置写 mod name;, rustc 优先找 src/name.rs, 然后再找 src/name/mod.rs (旧约定)。模块内的项默认是私有的; pub 向上一层公开: pub fn ...pub struct ...、struct 字段也可逐个 pubuse crate::path::to::ThingThing 引入当前作用域以便不带前缀使用; use std::collections::HashMap 是标准库的对应写法。crate 关键字指当前 crate 的根, super 指父模块, self 指当前模块。

小项目的操作规则: 按逻辑领域声明一个子模块 (mod pricing;mod parsing;)、每个子模块放一个 src/<name>.rs 文件、在 lib.rs 顶部用 pub use 把 binary crate 需要的少数几项重新导出。

library crate 与 binary crate

一个 Cargo 包 (package) 如有 src/lib.rs 就有一个 library crate, 如有 src/main.rs 就有一个 binary crate, 两者可以并存——这种情况下 binary 通过 use <crate_name>::... 依赖 library, 其中 <crate_name> 就是 Cargo.toml[package] name = ...。library 是你能 publish 到 crates.io 让别人依赖的; binary 是你能作为可执行文件分发的。本课的 worked example 两者都用: library 装 pricing::black_scholes_call 与单元测试; binary 在 main 里调 library。多 binary 用 src/bin/<name>.rs 这里点一句, forward-pointer 到 3.5.4; Cargo 工作空间——顶层 [workspace] 表绑定多个 crate——forward-pointer 到 3.5.4 与 3.6.5。

Cargo.toml

worked-example 项目的完整 manifest, 见下面 Fenced TOML 块:

[package]
name = "hs300_pricer"
version = "0.1.0"
edition = "2021"

[dependencies]
approx = "0.5"

[dev-dependencies]

[package] 节设 nameversionedition[dependencies] 节列运行时依赖; approx = "0.5" 拉取 approx crate 的 >= 0.5.0, < 0.6.0 区间版本 (caret semver 是默认的, "0.5""^0.5" 的简写)。[dev-dependencies] 节这里为空, 但 proptestcriterion 这类「仅测试时需要」的 crate 都放这里; 这里声明的依赖只在构建测试时链接, 下游用户使用你的 library 时永远看不到。[build-dependencies] 节为 build.rs 构建脚本服务, forward-pointer 到 3.5.4。

Cargo.lock 记录依赖树中每个 crate 的精确解析版本: binary crate 要 commit 它, 让 CI 与开发机的构建可比特对位; library crate 不 commit 它, 让下游消费者按自己的最新兼容版本解析。cargo build 头一次做解析、填充 target/ 目录, 后续构建走缓存——一个全新 crate 索引下的首次构建可能要一两分钟, 之后增量改动通常亚秒级。

src/lib.rs

//! 区域锚定的欧式 call 定价器。

pub mod pricing;

//! 起一段 crate 级 doc-comment; 空行隔开后 pub mod pricing; 声明并公开 pricing 子模块。library 入口就这几行, 真正的函数在另一文件里。

src/pricing.rs

/// 计算 Black-Scholes 欧式 call 价。
///
/// # Arguments
/// * `s` - spot price
/// * `k` - strike price
/// * `r` - risk-free rate (continuously compounded)
/// * `sigma` - volatility
/// * `t` - time to expiry in years
pub fn black_scholes_call(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
    // implementation from L2
    0.0
}

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

    #[test]
    fn test_call_atm() {
        let price = black_scholes_call(4.20, 4.20, 0.025, 0.22, 0.25);
        assert_abs_diff_eq!(price, 0.1854, epsilon = 1e-4);
    }
}

/// 是 doc-comment, 由 cargo doc --open 渲染成 HTML; # Arguments 是参数列表的惯例小标题。mod tests 上的 #[cfg(test)] 属性意为「只在 cargo test 时编译此块」——测试代码永远不会进生产二进制。块内的 use super::*; 把父模块所有项 (含私有项, 这才是单元测试的意义——能直接测私有 helper) 引入。每个测试函数前面打 #[test]approx::assert_abs_diff_eq!(price, 0.1854, epsilon = 1e-4) 替代天真的 assert_eq!(price, 0.1854)——永远不要对 f64assert_eq!, 浮点相等与数值相等是两件事, epsilon = 1e-4 才是「精确到四位小数」的正确写法。

src/main.rs

use hs300_pricer::pricing::black_scholes_call;

fn main() {
    let price = black_scholes_call(4.20, 4.30, 0.025, 0.22, 0.25);
    println!("price = {:.4}", price);
}

use hs300_pricer::pricing::black_scholes_call; 从 library crate (Cargo.toml 里 name 为 hs300_pricer) 里把函数引入; binary 在 main 里调它。数值输入沿用 L2 的 510300.SH 沪深300 ETF 期权场景。cargo run --release 跑出与 L2 二进制相同的答案——但现在以 library 形态组织, 任何地方都能调、任何地方都能测。

日常 Cargo 循环, 加大版

cargo new --lib hs300_pricer
cargo build
cargo test
cargo fmt
cargo clippy -- -D warnings
cargo doc --open
cargo run --release

cargo new --lib 初始化一个纯 library 项目; 加 src/main.rs 就变成 library + binary 双结构。cargo build 解析依赖并编译。cargo test 编出测试二进制并跑所有 #[test] 函数, 覆盖单元测试、集成测试与 doctest 三层, 输出分三段。

集成测试

集成测试住在项目顶层的 tests/ 目录, 一个测试目标一个文件。每个文件 单独 编成一个二进制, 依赖 library crate、只能访问其 公开 API——这就是外部用户能触及的表面。示例 tests/pricing_integration.rs:

use hs300_pricer::pricing::black_scholes_call;
use approx::assert_abs_diff_eq;

#[test]
fn integration_call_price_is_positive_for_atm() {
    let price = black_scholes_call(4.20, 4.20, 0.025, 0.22, 0.25);
    assert!(price > 0.0);
    assert_abs_diff_eq!(price, 0.1854, epsilon = 1e-3);
}

经验法则: 测试需要触碰私有 helper 就是单元测试 (与代码同文件、use super::*; 进模块内部); 测试 end-to-end 走 library 的公开 API 就是集成测试 (在 tests/)。两层叠加, 你能放心重构内部 helper, 集成测试钉死公开契约。

日常质量工具链

cargo fmt (底层调 rustfmt) 自动应用项目风格。默认风格是社区标准; 项目级微调写在项目根 rustfmt.toml。每次 commit 前跑一次 cargo fmt; CI 上跑 cargo fmt --check, 有任何需要重排的地方就失败构建。

cargo clippy 是 lint 工具。它检查上百种「编译器不抱怨但代码评审会抱怨」的模式: 冗余克隆、低效写法 (例如 vec.iter().count() 而非 vec.len())、可疑的 cast 等等。CI 日常调用 cargo clippy -- -D warnings——把所有 warning 当 error。文件 / 项目级 lint 可以用 #[allow(clippy::lint_name)] 静默, 但要有充分理由——理解 lint 之前不要伸手关它。

cargo doc --open/// doc-comment 生成 HTML 并在浏览器打开。doc-comment 里的 fenced ```rust 代码块 也会cargo test 当 doctest 运行——标准库自己就这么做, 生产 Rust 代码也该这么做。给 black_scholes_call 加一段四行的 doc-comment, 跑 cargo doc --open 看渲染页, 顺手让 doctest 把文档钉准。

行业收束

国内头部基础设施与中频私募团队的 Rust 项目 CI 流水线几乎都收敛到同一套规约: 在多套环境下分别用最小核数与生产核数各跑一次, 把可重现性与并发隐患都覆盖到。具体到子命令, PingCAP、蚂蚁链、字节火山引擎以及部分私募的 Rust 项目 CI 几乎清一色跑这三条: cargo fmt --checkcargo clippy --all-targets --all-features -- -D warningscargo test --all-features --workspace, 再额外加 cargo deny check 做 license / advisory / 禁用 crate 检查 (这块留到 3.6.2 Git & Code Quality)。本课的 worked example 在单 crate 尺度演示了前三条, 多 crate workspace 的扩展放到 Module 3.5.4。到这里你已经完成 Module 3.5.1: 能写、能编、能组织、能测、能格式化、能 lint、能生成文档一个 Rust 项目。Module 3.5.2 从并发开始。

练习

Exercise

拿本课的 worked-example 项目: (a) 在 src/pricing.rs 里加第二个公开函数 pub fn black_scholes_put(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 计算欧式 put 价 (直接闭式公式或由 call 价走 put-call parity 都行), 配同款 # Arguments/// doc-comment; (b) 在已有的 #[cfg(test)] mod tests 块里给 put 加三条单元测试 (deep-ITM、ATM、deep-OTM); (c) 在 tests/put_call_parity.rs 加一条集成测试, 用 assert_abs_diff_eq! 断言 call_price - put_price 等于 s - k * (-r * t).exp()epsilon = 1e-6 内; (d) 依次跑 cargo fmtcargo clippy -- -D warningscargo test, 报告这三条中哪几条在成功前产生了输出; (e) 故意把 put 公式里某个 + 写成 -, 再跑 cargo test, 逐字抄下失败断言信息, 然后改回来确认全绿。

提示
关于 (b): 三组输入为 deep-ITM (s = 3.0, k = 4.2)、ATM (s = 4.2, k = 4.2)、deep-OTM (s = 5.0, k = 4.2)——put 侧 ITM 与 OTM 与 call 相反。参考值可由 call 测试值经 put-call parity 反推得到。
提示
关于 (d): 预期 cargo fmt 在已格式化的代码上零输出; cargo clippy 干净时不打 warning; cargo test 打三段汇总。代码干净时 test result: ok 之前没有诊断输出。