尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Rust By Example:用 Cargo 组织与运行单元测试、集成测试与文档测试的完整指南

Rust By Example:用 Cargo 组织与运行单元测试、集成测试与文档测试的完整指南 文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载测试是任何软件质量保障的基石Rust 语言本身就对单元测试、集成测试与文档测试提供了“一等公民”般的原生支持。本文以 rust-by-example 仓库的 Cargo 测试章节 为核心骨架结合仓库 Testing 章节 及其四个子章节系统讲解测试在 Cargo 工程中的目录组织方式、cargo test的完整用法与输出解析、按名称过滤测试、以及 Cargo 并发执行测试带来的竞态风险与规避方案。读完本文你将能够为自己的 Rust 工程搭建规范的测试体系并熟练运用过滤、忽略、断言与文档测试等实战技巧。Cargo 与测试从生态到命令的一体化支持在深入测试细节之前先回顾 Cargo 在整个 Rust 生态中的定位。Cargo 章节 明确指出cargo是 Rust 官方包管理工具除了依赖管理与 crates.ioRust 官方包仓库集成外它天然“感知”单元测试与基准测试benchmarks这意味着你不需要任何第三方测试框架直接用cargo test即可运行工程内全部测试。Cargo 对测试的原生支持体现在两个方面依赖管理层面通过[dev-dependencies]可以声明仅用于测试或示例、基准的依赖这些依赖不会传播给依赖本包的其它包详见后文与 dev-dependencies 章节工程组织层面Cargo 约定了一套标准目录布局见 Conventions 章节测试代码被安放在约定位置后cargo test会自动发现并执行它们。测试的目录组织单元测试进模块集成测试进 tests/rust-by-example 的 Cargo 测试章节 给出了测试代码的标准组织原则组织上我们把单元测试放在它们所测试的模块内部把集成测试放在独立的tests/目录中。一个典型的工程布局如下foo ├── Cargo.toml ├── src │ └── main.rs │ └── lib.rs └── tests ├── my_test.rs └── my_other_test.rs其中每个文件的职责清晰可辨src/main.rs、src/lib.rs业务源码单元测试以#[cfg(test)] mod tests的形式内嵌在被测模块内详见 unit_testing.mdtests/目录下的每个文件都是一个独立的集成测试。集成测试以“外部调用者”身份验证公共接口Integration testing 章节 对集成测试做了精确定义集成测试独立于你的 crate 之外只能像任何其它使用方代码一样调用它的公共接口目的是验证库的多个部分能否协同工作。相比之下单元测试一次只隔离测试一个模块规模小且可以测试私有代码。因此Cargo 测试章节 特别强调tests/目录中的每个文件都是一个独立的集成测试即“把库当作被外部依赖 crate 调用时那样进行测试”。一个完整的集成测试示例crate 名为adder文件src/lib.rs// 在名为 adder 的 crate 中定义此函数 pub fn add(a: i32, b: i32) - i32 { a b }文件tests/integration_test.rs#[test] fn test_add() { assert_eq!(adder::add(3, 2), 5); }注意集成测试通过adder::add这样的 crate 路径调用公开 API与外部使用者完全一致。tests/目录中每个 Rust 源文件都会被编译成一个独立的 crate。若想在多个集成测试间共享公共代码如环境准备逻辑正确做法是创建一个tests/common/mod.rs模块文件tests/common/mod.rspub fn setup() { // 一些初始化代码比如创建所需文件/目录、启动服务器等 }文件tests/integration_test.rs// 导入 common 模块 mod common; #[test] fn test_add() { // 使用 common 中的代码 common::setup(); assert_eq!(adder::add(3, 2), 5); }此处有一条重要经验把共享模块写成tests/common.rs虽然也能工作但不推荐——因为测试运行器会把该文件当作一个测试 crate 对待并尝试运行其中的“测试”造成干扰。写成tests/common/mod.rs则会被 Cargo 识别为共享模块而不会独立编译成测试二进制。cargo test一条命令运行全部测试Cargo 为运行全部测试提供了最直接的入口这也是 Cargo 测试章节 的核心命令$ cargo test以文档中的blah工程为例典型输出如下$ cargo test Compiling blah v0.1.0 (file:///nobackup/blah) Finished dev [unoptimized debuginfo] target(s) in 0.89 secs Running target/debug/deps/blah-d3b32b97275ec472 running 4 tests test test_bar ... ok test test_baz ... ok test test_foo_bar ... ok test test_foo ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out这段输出包含大量可解读的信息编译阶段Compiling与Finished dev [unoptimized debuginfo]说明cargo test默认以 dev profile未优化、含调试信息编译测试测试二进制Running target/debug/deps/blah-d3b32b97275ec472是被执行的测试可执行文件名称带哈希以区分不同构建逐条结果test 名称 ... ok列出每个测试的判定汇总行test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out依次给出通过数、失败数、忽略数#[ignore]、测量数基准测量与过滤数被名称过滤掉的测试数。从仓库的 unit_testing.md 可以看到若测试失败输出中会附上失败详情与断言信息$ cargo test running 2 tests test tests::test_bad_add ... FAILED test tests::test_add ... ok failures: ---- tests::test_bad_add stdout ---- thread tests::test_bad_add panicked at assertion failed: (left right) left: -1, right: 3, src/lib.rs:21:8 note: Run with RUST_BACKTRACE1 for a backtrace. failures: tests::test_bad_add test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out按名称模式过滤测试当工程测试较多时只运行部分测试会更高效。Cargo 测试章节 演示了最简单的过滤方式把名称片段作为参数传给cargo testCargo 会运行名称匹配该模式的所有测试。$ cargo test test_foo$ cargo test test_foo Compiling blah v0.1.0 (file:///nobackup/blah) Finished dev [unoptimized debuginfo] target(s) in 0.35 secs Running target/debug/deps/blah-d3b32b97275ec472 running 2 tests test test_foo ... ok test test_foo_bar ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out注意上例尽管只指定了test_foo但名为test_foo_bar的测试也被运行了——因为它是子串匹配。汇总行末尾的2 filtered out表明其余两个测试被过滤。unit_testing.md 进一步展示了过滤的两种用法指定完整名称只运行单个测试$ cargo test test_any_panic running 1 test test tests::test_any_panic ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out指定名称片段批量运行相关测试例如所有名字含panic的测试$ cargo test panic running 3 tests test tests::test_any_panic ... ok test tests::test_specific_panic ... ok test tests::test_specific_panic_shorthand ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 1 filtered outcargo test还支持把参数传给测试运行器例如cargo test -- --ignored专门运行被#[ignore]标记的测试详见 unit_testing.md 的“Ignoring tests”小节。测试内容的三要素断言、?与#[should_panic]集成测试与单元测试都依托 unit_testing.md 讲解的断言体系。测试是验证非测试代码按预期工作的 Rust 函数函数体通常先做一些准备setup运行被测代码再断言结果是否符合预期。测试函数在发生 panic 时即失败因此常用以下辅助宏assert!(expression)表达式求值为false时 panicassert_eq!(left, right)左右表达式不等时 panicassert_ne!(left, right)左右表达式相等时 panic。配合#[cfg(test)]仅在测试构建下编译该模块与#[test]标记测试函数即可写出标准单元测试。一个值得注意的细节是私有函数也可以被测试——同文件模块内的测试可以访问被测模块的私有项pub fn add(a: i32, b: i32) - i32 { a b } // 这是一个很糟糕的加法函数其目的是在本例中失败。 #[allow(dead_code)] fn bad_add(a: i32, b: i32) - i32 { a - b } #[cfg(test)] mod tests { // 注意这个惯用法从外层mod tests 的作用域导入名字。 use super::*; #[test] fn test_add() { assert_eq!(add(1, 2), 3); } #[test] fn test_bad_add() { // 这个断言会触发并使测试失败。 assert_eq!(bad_add(1, 2), 3); } }让测试返回 Result直接用?从 Rust 2018 起测试函数可以返回Result()从而在测试体内直接使用?运算符使测试更简洁fn sqrt(number: f64) - Resultf64, String { if number 0.0 { Ok(number.powf(0.5)) } else { Err(negative floats dont have square roots.to_owned()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_sqrt() - Result(), String { let x 4.0; assert_eq!(sqrt(x)?.powf(2.0), x); Ok(()) } }断言 panic#[should_panic]对于特定条件下应当 panic的函数使用#[should_panic]属性标记测试。该属性还接受可选参数expected 指定期望的 panic 消息文本——当函数可能以多种方式 panic 时这能确保测试验证的是正确的那个 panicpub fn divide_non_zero_result(a: u32, b: u32) - u32 { if b 0 { panic!(Divide-by-zero error); } else if a b { panic!(Divide result is zero); } a / b } #[cfg(test)] mod tests { use super::*; #[test] fn test_divide() { assert_eq!(divide_non_zero_result(10, 2), 5); } #[test] #[should_panic] fn test_any_panic() { divide_non_zero_result(1, 0); } #[test] #[should_panic(expected Divide result is zero)] fn test_specific_panic() { divide_non_zero_result(1, 10); } #[test] #[should_panic Divide result is zero] // 这种写法同样有效 fn test_specific_panic_shorthand() { divide_non_zero_result(1, 10); } }Rust 还支持简写形式#[should_panic message]它与#[should_panic(expected message)]完全等价两种写法均合法其中带expected 的写法更常用、也更显式。忽略测试#[ignore]对暂时不需要执行的测试可用#[ignore]属性将其排除在常规运行之外之后再用cargo test -- --ignored专门运行它们pub fn add(a: i32, b: i32) - i32 { a b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 2), 4); } #[test] fn test_add_hundred() { assert_eq!(add(100, 2), 102); assert_eq!(add(2, 100), 102); } #[test] #[ignore] fn ignored_test() { assert_eq!(add(0, 0), 0); } }$ cargo test running 3 tests test tests::ignored_test ... ignored test tests::test_add ... ok test tests::test_add_hundred ... ok test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out Doc-tests tmp-ignore running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out $ cargo test -- --ignored running 1 test test tests::ignored_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out从这两段输出可以看到一个有趣的细节测试结果下方还有一行Doc-tests tmp-ignore—— 这正是cargo test顺带运行文档测试doc-tests的表现。文档测试让文档注释里的代码片段可编译、可运行Rust 项目最主要的文档形式是对源码的注释标注文档注释遵循 CommonMark Markdown 规范并支持代码块。Rust 负责校验这些代码块的正确性——它们会被编译并作为文档测试执行。这就是 doc_testing.md 的主题也解释了为什么每次cargo test输出末尾都会出现Doc-tests crate名段落。/// 第一行是描述函数的简短摘要。 /// /// 接下来的行提供详细文档。代码块以三个反引号开头内部隐式包含 /// fn main() 和 extern crate cratename。 /// /// /// let result playground::add(2, 3); /// assert_eq!(result, 5); /// pub fn add(a: i32, b: i32) - i32 { a b }文档测试随普通cargo test自动执行$ cargo test running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests playground running 3 tests test src/lib.rs - add (line 7) ... ok test src/lib.rs - div (line 21) ... ok test src/lib.rs - div (line 31) ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out文档测试存在一个常见痛点示例代码常想使用?但?要求函数返回Result而文档代码块隐式的main返回单元类型直接使用会编译失败。解决办法是用#前缀隐藏辅助行编写一个隐藏的try_main() - Result(), ErrorType再在隐藏的main里unwrap它/// 在文档测试中使用隐藏的 try_main。 /// /// /// # // 以 # 开头的行是隐藏行但依然参与编译 /// # fn try_main() - Result(), String { // 包裹文档中展示的函数体的行 /// let res playground::try_div(10, 2)?; /// # Ok(()) // 从 try_main 返回 /// # } /// # fn main() { // 启动 main将执行 unwrap() /// # try_main().unwrap(); // 调用 try_main 并 unwrap /// # // 以便出错时测试 panic /// # } /// pub fn try_div(a: i32, b: i32) - Resulti32, String { if b 0 { Err(String::from(Divide-by-zero)) } else { Ok(a / b) } }测试专用依赖[dev-dependencies]有时某些依赖仅测试或示例、基准需要生产代码并不使用。dev_dependencies.md 指出这类依赖应加入Cargo.toml的[dev-dependencies]段且不会被传播给依赖本包的其它包。一个典型例子是pretty_assertions——它扩展标准assert_eq!/assert_ne!宏输出彩色差异对比。文件Cargo.toml# 标准 crate 数据在此省略 [dev-dependencies] pretty_assertions 1文件src/lib.rspub fn add(a: i32, b: i32) - i32 { a b } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; // 仅测试用 crate不能在非测试代码中使用 #[test] fn test_add() { assert_eq!(add(2, 3), 5); } }注意use pretty_assertions::assert_eq;只在#[cfg(test)] mod tests内生效这正是[dev-dependencies]的核心语义——该依赖只在测试构建中存在普通构建完全感知不到它。并发执行的警告测试之间可能互相竞争Cargo 测试章节 在文末给出了一条重要告诫Cargo 可能并发运行多个测试所以务必确保它们不会互相竞争race。文档给出一个经典竞争示例两个测试同时向同一个文件追加内容。尽管开发者意图是先写完Ferris五行、再写Corro五行#[cfg(test)] mod tests { // 导入必要的模块 use std::fs::OpenOptions; use std::io::Write; // 该测试向文件写入内容 #[test] fn test_file() { // 打开 ferris.txt若不存在则创建它 let mut file OpenOptions::new() .append(true) .create(true) .open(ferris.txt) .expect(Failed to open ferris.txt); // 打印 Ferris 5 次 for _ in 0..5 { file.write_all(Ferris\n.as_bytes()) .expect(Could not write to ferris.txt); } } // 该测试尝试向同一个文件写入内容 #[test] fn test_file_also() { // 打开 ferris.txt若不存在则创建它 let mut file OpenOptions::new() .append(true) .create(true) .open(ferris.txt) .expect(Failed to open ferris.txt); // 打印 Corro 5 次 for _ in 0..5 { file.write_all(Corro\n.as_bytes()) .expect(Could not write to ferris.txt); } } }期望的文件内容是$ cat ferris.txt Ferris Ferris Ferris Ferris Ferris Corro Corro Corro Corro Corro但实际写入ferris.txt的内容却是两行交替穿插$ cargo test test_file cat ferris.txt Corro Ferris Corro Ferris Corro Ferris Corro Ferris Corro Ferris这个例子生动揭示了并发执行的破坏力两个测试进程交错执行write_all破坏了文件内容的整体性。规避方案包括让每个测试使用独立的输出文件文件名可加入测试名或临时目录或借助共享模块如前面提到的tests/common/mod.rs中的setup()做串行化与资源清理或使用cargo test -- --test-threads1之类的方式限制测试并发具体选项以当前 Cargo/测试运行器版本为准。核心原则始终是测试之间不能共享可变的外部状态。小结一套完整可落地的 Rust 测试工作流综合 Cargo 测试章节 与 Testing 章节 的全部内容一个规范的 Rust 工程测试体系可以归纳为组织单元测试内嵌于被测模块的#[cfg(test)] mod tests中可测私有函数集成测试放在tests/目录每个文件一个独立 crate只走公共接口共享测试代码放进tests/common/mod.rs运行cargo test一条命令跑完全部单元、集成与文档测试用cargo test 名称片段过滤用cargo test -- --ignored专门跑被忽略的测试断言熟练使用assert!、assert_eq!、assert_ne!让测试返回Result()以使用?用#[should_panic(expected ...)]验证特定 panic依赖仅测试用的依赖放入[dev-dependencies]避免污染下游使用者并发安全时刻警惕 Cargo 并发执行测试的默认行为避免测试之间因共享文件、端口等外部资源而发生竞争。这套工作流完全内置于 Rust 官方工具链无需任何第三方测试框架即可在任意 Cargo 工程中直接使用。延伸阅读仓库内相关章节Testing 章节总览三种测试风格单元 / 文档 / 集成与 dev-dependencies 的入口页unit_testing.md断言宏、Result()测试、#[should_panic]、#[ignore]的完整代码示例integration_testing.mdtests/目录语义与tests/common/mod.rs共享模块实践doc_testing.md文档注释、代码块测试与隐藏行#技巧dev_dependencies.md测试专用依赖的声明与使用conventions.mdCargo 工程目录约定src/bin/多二进制、测试与示例布局deps.mdCargo.toml中[dependencies]的声明方式crates.io、git、本地路径。赞分享文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载相关推荐cargo test 完全指南Cargo 的单元测试、集成测试与文档测试实战与底层原理cargo test 完全指南Cargo 的单元测试、集成测试与文档测试实战与底层原理 cargo test 是 Rust 包管理器 Cargo 中执行测试的开发工具包管理器CLI构建工具从 cargo test 看 Cargo 的测试体系单元测试、集成测试与文档测试全解析从 cargo test 看 Cargo 的测试体系单元测试、集成测试与文档测试全解析 cargo test 是 Cargo 提供的统一测试入口一条命令即可开发工具包管理器CLI构建工具react-bootstrap组件测试单元测试与集成测试完整指南react bootstrap组件测试单元测试与集成测试完整指南 在React应用开发中组件测试是确保UI稳定性和功能正确性的关键环节。react boot前端UI组件上一篇VMware Unlocker项目中的Darwin工具ISO缺失问题解析下一篇YOLOv7技术深度解析从实时目标检测到3D感知的完整实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表