24-测试
23 分钟阅读
测试 (Testing)
面向 Rust 1.97.1 (stable, 2026-07)。本篇假设你熟悉 Go,凡有可比性处均给出 🐹 Go 对比。
热度:
hot高频 |common常见 |occasional偶尔 |advanced进阶少见
本篇能解决什么:
- 你会不会分不清:单元测试、集成测试、文档测试该写在哪、能不能测私有函数?
- 你是否想知道:
#[cfg(test)] mod tests怎么写,和 Go 的_test.go差在哪? - 你是否分不清
assert!/assert_eq!/assert_ne!/debug_assert!该用哪个? - 你是否想只跑一个测试、看
println!输出,却不知道cargo test的过滤与--参数? - 你是否想把 Go 的表驱动测试平移到 Rust,或写回归测试锁住修过的 bug?
- 你会不会在 CI 里
cargo test偶发失败、锁文件漂移,不知道该加哪些开关? - 你是否想做属性测试、mock HTTP 上游、或测 CLI 二进制(
proptest/ wiremock /assert_cmd)?
术语速查表:
| 术语 / 缩略词 | 全称 / 读法 | 中文 | 一句话解释 | Go 里的近亲 |
|---|---|---|---|---|
| unit test | — | 单元测试 | 通常写在源码旁,可测私有项 | 同包 _test.go |
| integration test | — | 集成测试 | tests/ 下独立 crate,只测公共 API | 外部测试包 package foo_test |
| doctest | documentation test | 文档测试 | 文档注释里的代码块会被编译执行 | ExampleXxx |
| test harness | — | 测试运行器 | cargo test 背后收集并跑 #[test] 的程序 | go test 运行器 |
#[cfg(test)] | configuration attribute | 仅测试配置 | 只在 cargo test 时编译该块 | 类似 //go:build 限定测试文件 |
assert_eq! | — | 相等断言宏 | 失败时打印左右两边的值 | if got != want { t.Fatalf(...) } |
should_panic | — | 期望 panic | 声明测试按设计应 panic | defer + recover |
nocapture | — | 不捕获输出 | 让测试的 stdout/stderr 实时打印 | go test -v 的一部分效果 |
| table-driven | — | 表驱动测试 | 一组输入/期望值循环跑同一逻辑 | Go 经典表驱动 |
| CI | Continuous Integration | 持续集成 | 推送/PR 时自动跑测试的流水线 | 同概念 |
| mock / test double | — | 测试替身 | 测试里替换真实依赖的假实现 | interface + fake / gomock |
mockall | — | mock 生成库 | 从 trait 生成可编排的 mock 类型 | gomock / testify/mock |
| proptest | — | 属性测试库 | 随机生成输入验证不变量 | testing/quickcheck 一类 |
| wiremock | — | HTTP mock 服务 | 测试里起假 HTTP 上游 | httptest + 自研 mock server |
| assert_cmd | — | CLI 断言库 | 跑二进制并断言退出码/输出 | 测 main 的黑盒方式 |
说明:本表覆盖本篇出现的所有专业名词与缩略词;正文首次出现时仍会就地解释一次。
热度索引:
| 热度 | 题目 |
|---|---|
hot | Q1, Q2, Q3, Q4, Q8, Q10, Q21, Q22 |
common | Q5, Q6, Q7, Q9, Q11, Q13, Q14, Q15, Q16, Q17, Q19, Q20, Q23 |
occasional | Q12, Q18 |
advanced | — |
Q1. 单元测试、集成测试、文档测试分别放哪?可见性有什么差别?
Tags: hot beginner unit-test integration-test doctest
适用版本: Rust 1.0+
一句话答案:
单元测试写在源码旁(常放 #[cfg(test)] mod tests),能测私有项;集成测试放 tests/,是独立 crate,只能测公共 API;文档测试写在 /// 代码块里,随 cargo test 一起跑。
解答: 三者的差别不只是“文件放哪儿”,更是可见性边界不同。
| 种类 | 常见位置 | 可见性 | 和被测代码的关系 |
|---|---|---|---|
| 单元测试 | src/*.rs 内 | 可访问 pub(crate) / 私有项(同模块树) | 同一 crate |
| 集成测试 | tests/*.rs | 只能 use 公共 API | 每个文件 ≈ 独立 crate |
| 文档测试 | /// / //! 里的 markdown 代码块 | 只能用文档里能写出来的公共用法 | 像“用户复制粘贴” |
典型布局:
| |
源码内单元测试示意(完整可测片段需放在 lib/crate 里;下面是常见写法):
| |
文档测试同时是示例和回归:
| |
Go 对比:
- Go 怎么做:同包
_test.go可测未导出符号;package foo_test只能测导出 API;另有Example函数。 - Rust 为什么不同:把“源码内测试 /
tests/黑盒 / 文档即测试”三套路径都挂在统一的cargo test上。 - Go 程序员易踩的坑:以为
tests/里也能测私有函数——不能,那是独立 crate。
记忆点:
- 私有实现细节 → 源码内单元测试。
- 公共契约 →
tests/集成测试。 - 给用户看的最小用法 → 文档测试。
Q2. #[cfg(test)] mod tests 标准写法是什么?
Tags: hot beginner cfg(test)
适用版本: Rust 1.0+
一句话答案:
在模块末尾加 #[cfg(test)] mod tests { use super::*; #[test] fn ... }:只在跑测试时编译,并用 super::* 引入被测项。
解答:
#[cfg(test)](configuration attribute,配置属性)表示:这段代码只在 测试配置 下编译——cargo build / cargo run 不会带上它,cargo test 才会。
| |
#[test] 标记的函数由 test harness(测试运行器)发现并执行;函数名会出现在 cargo test 输出里,也是过滤用的名字。
「❌ 错误写法」——把 mod tests 写成普通模块却忘记 #[cfg(test)]:正式构建也会编译测试代码,还可能拉进仅测试用的依赖。
「✅ 正确写法」——始终加 #[cfg(test)];需要共享测试辅助函数时,可再拆 #[cfg(test)] mod common; 之类,同样只在测试时编译。
Go 对比:
- Go 怎么做:文件名以
_test.go结尾,构建时自动排除出普通包。 - Rust 为什么不同:测试可以嵌在同一
.rs文件里,所以用属性控制“何时编译”,而不是靠文件名后缀。 - Go 程序员易踩的坑:到处新建
*_test.rs文件名——Rust 不认这个约定;集成测试目录才是tests/。
记忆点:
#[cfg(test)]+mod tests+use super::*+#[test]。- 测试模块默认是子模块,能访问父模块私有项。
Q3. assert!、assert_eq!、assert_ne!、debug_assert! 怎么选?
Tags: hot assert
适用版本: Rust 1.0+
一句话答案:
布尔条件用 assert!;比相等用 assert_eq!;比不等用 assert_ne!;只想在 debug 构建保留的内部检查用 debug_assert!(release 默认关掉)。
解答:
失败时 assert_eq! / assert_ne! 会打印左右值,排查通常比纯布尔 assert! 更省事。
| |
debug_assert! 适合“开发期自检、生产期嫌贵”的不变量;不能把它当成业务正确性的唯一保障。
| |
在测试里一般直接用 assert! / assert_eq!:测试本来就该在所有配置下执行断言。debug_assert! 更适合库内部热路径上的可选检查。
Go 对比:
- Go 怎么做:常见
if got != want { t.Fatalf("got %v want %v", got, want) }。 - Rust 为什么不同:标准库宏统一了相等/不等失败信息格式。
- Go 程序员易踩的坑:习惯写
assert!(a == b)——能用,但失败时看不到a/b的值;优先assert_eq!。
记忆点:
- 比两个值 →
assert_eq!/assert_ne!。 - 比条件 →
assert!。 debug_assert!≠ 生产业务断言。
Q4. 怎么只跑一个测试?--exact、--nocapture、--test-threads=1 分别干什么?
Tags: hot cargo-test filter
适用版本: Cargo(随 rustup 工具链)
一句话答案:
用名字子串过滤;-- --exact 精确匹配全名;-- --nocapture 实时看 stdout;-- --test-threads=1 强制串行。注意:-- 后面才是传给测试 harness 的参数。
解答:
cargo test 的参数分两段:Cargo 自己的选项,以及 -- 之后转发给 harness 的选项。
| |
组合也很常见:
| |
对应 Go 的肌肉记忆:go test -run '^TestFoo$' -v ≈ 过滤 + 详细输出;Rust 把“详细输出”拆成了默认捕获 + 可选 nocapture。
Go 对比:
- Go 怎么做:
go test -run TestName、-v、-parallel 1/t.Parallel()控制。 - Rust 为什么不同:Cargo 与 harness 参数用
--分隔,初学者常把--nocapture写在--前面导致无效。 - Go 程序员易踩的坑:写成
cargo test --nocapture(少了中间的--)。
记忆点:
- 过滤名在
cargo test <filter>。 - harness 开关一律
cargo test -- <flags>。 - 串行是排查共享状态的手段,不是长期默认。
Q5. #[should_panic] 适合测什么?怎么写 expected?
Tags: common should_panic
适用版本: Rust 1.0+
一句话答案:
只适合“按 API 契约就应该 panic”的路径;用 expected = "..." 锁定 panic 消息片段,避免“换了个无关 panic 却仍算通过”。
解答:
普通业务失败应测 Result::Err,不要用 panic 冒充错误处理。
| |
边界检查类 panic:
| |
Go 对比:
- Go 怎么做:
defer func(){ if recover() == nil { t.Fatal(...) } }()。 - Rust 为什么不同:把“期望崩溃”做成一等测试属性。
- Go 程序员易踩的坑:把所有
Err路径都改成 panic 再should_panic——风格和可测试性都会变差。
记忆点:
- 契约是 panic →
#[should_panic(expected = "...")]。 - 契约是错误值 → 测
Result。
Q6. #[ignore] 干什么?怎么跑被忽略的测试?
Tags: common ignore
适用版本: Rust 1.0+;--include-ignored 为较新 harness 行为(日常 Cargo 均支持)
一句话答案:
#[ignore] 标记默认不跑的慢测/需外部资源的测;需要时用 cargo test -- --ignored 或 -- --include-ignored。
解答:
| |
| |
适合:慢、贵、依赖密钥/真机服务、或会污染环境的用例。日常 cargo test 应保持快反馈。
Go 对比:
- Go 怎么做:
testing.Short()、build tag、或手写t.Skip。 - Rust 为什么不同:用属性 + harness 开关集中控制,入口统一。
- Go 程序员易踩的坑:把不该默认跑的重测留在主路径,拖慢每个人的本地循环。
记忆点:
- 慢测 / 外依赖 →
#[ignore]。 - CI 夜间任务可显式
--ignored。
Q7. 测试函数可以返回 Result 吗?为什么方便?
Tags: common Result ?
适用版本: Rust 1.0+(测试里用 ? 的习惯随时代普及)
一句话答案:
可以:fn test_foo() -> Result<(), E>,失败时 harness 会把 Err 当成测试失败;这样能在测试里直接用 ?,少写一堆 unwrap。
解答:
| |
注意:返回 Result 适合“设置阶段可能失败”的测试;断言失败仍用 assert_*!(它们是 panic,不是 Err)。
Go 对比:
- Go 怎么做:
got, err := f(); if err != nil { t.Fatal(err) }。 - Rust 为什么不同:测试签名允许
Result,?直接把错误冒泡给 harness。 - Go 程序员易踩的坑:全程
unwrap(),失败信息只剩 panic,不如?+ 明确Err清晰。
记忆点:
- 准备数据用
?;断言用assert_*!;最后Ok(())。
Q8. 为什么集成测试只能测公共 API?私有函数测不了怎么办?
Tags: hot integration-test visibility
适用版本: Rust 1.0+
一句话答案:
tests/*.rs 每个文件都是依赖你库的独立 crate,和外部用户一样,只能 use 你的 pub 项;私有逻辑请放单元测试,或抽成可测的小 pub(crate) / 子模块 API。
解答:
假设库名是 billing:
| |
| |
「❌」在 tests/ 里写 use billing::secret_discount——私有项不可见,编译失败。
「✅」两种合法策略:
- 私有细节用
src内[cfg(test)]单元测试覆盖(见 Q2)。 - 若必须从外部测,把它提升为有意的公共(或
pub(crate)再配合单元测试)API,并写清文档。
Go 对比:
- Go 怎么做:
package billing_test同样只能测导出符号;同包测试可测未导出。 - Rust 为什么不同:
tests/≈ 永远是外部测试包;同文件/子模块单元测试承担“白盒”角色。 - Go 程序员易踩的坑:把所有测试都扔进
tests/,结果大量实现细节测不到。
记忆点:
- 集成测试 = 黑盒用户视角。
- 白盒 = 源码内
#[cfg(test)]。
Q9. 文档测试最值得测什么?ignore / no_run 何时用?
Tags: common doctest
适用版本: Rust 1.0+
一句话答案:
最值得测公共 API 的最短正确用法;ignore 跳过执行(仍可文档展示),no_run 只编译不运行(适合会阻塞或需环境的示例)。
解答: 文档测试失败 = 用户复制的第一段示例就挂了,信任成本很高。
文档注释里写可执行示例(以下为库代码示意;真正的文档测试代码块写在 /// 下面):
| |
特殊标记写在文档代码块的语言标签旁:
ignore:跳过该示例(仍可展示在文档里)no_run:只编译、不执行(适合会阻塞或依赖外部环境的示例)- 默认可跑:最短公共 API 用法,随
cargo test/cargo test --doc执行
例如:需要网络的示例标 ignore;只想证明 TcpListener::bind 能编译、不要真 listen 的示例标 no_run。
还有 compile_fail(故意展示编译失败示例)等,日常库文档最常用的是默认可跑、no_run、ignore。
Go 对比:
- Go 怎么做:
Example函数,输出用注释里的// Output:比对。 - Rust 为什么不同:几乎任何
///代码块默认可测,社区更强调“文档能编译”。 - Go 程序员易踩的坑:文档里贴伪代码,Rust 里默认会挂
cargo test。
记忆点:
- 文档示例要短、真、跟 API 同步。
- 编译即可 / 别执行 →
no_run;完全跳过 →ignore。
Q10. 怎么写表驱动测试?和 Go 的 table-driven 怎么对应?
Tags: hot table-driven
适用版本: Rust 1.0+
一句话答案:
用数组/切片存 (输入, 期望),for 循环里 assert_eq!;失败时在断言消息里带上 case 名,定位方式和 Go 几乎一样。
解答:
| |
也可以每个 case 一个小闭包/结构体字段,但“一张表 + 循环”对 Go 转来的人最熟。
| |
Go 对比:
- Go 怎么做:
| |
- Rust 为什么不同:没有内建
t.Run子测试分层(可用第三方或宏,但标准库够用就是循环 + 消息)。 - Go 程序员易踩的坑:忘了在
assert_eq!里带 case 名,失败时不知道是哪一行数据。
记忆点:
- 表 = 数据;循环 = 同一断言。
- 断言消息带上
name。
Q11. cargo test --lib / --test / --doc 分别跑哪些?
Tags: common cargo-test filter-target
适用版本: Cargo
一句话答案:
--lib 只跑库的单元测试;--test <name> 只跑 tests/<name>.rs 那个集成测试目标;--doc 只跑文档测试。用来缩小范围、加快反馈。
解答:
| |
过滤名仍可叠加:
| |
Go 对比:
- Go 怎么做:
go test ./...、指定包路径、-run过滤。 - Rust 为什么不同:Cargo 按 target(lib / bin / test / example / doc)切开,比“按目录包”更细。
- Go 程序员易踩的坑:文档测试挂了却一直只跑
--lib,误以为全绿。
记忆点:
- 改实现细节 →
--lib。 - 改公共 API 契约 →
--test/ 全量。 - 改文档示例 →
--doc。
Q12. 异步测试怎么写?一定要 tokio 吗?
Tags: occasional async
适用版本: 稳定 Rust 本身不提供 async test 属性;需选运行时生态(如 Tokio)
一句话答案:
标准库没有 #[async_test];常见做法是依赖 Tokio 等运行时的 #[tokio::test],或自己在同步 #[test] 里 block_on。这是生态选择,不是语言内建。
解答:
最小依赖示意(需在 Cargo.toml 加 dev-dependency):
| |
| |
若不想在测试里拉宏,也可以:
| |
异步主题细节见 31-async-programming;本篇只强调:测 async 代码 = 先选定 runtime,再按其测试宏/阻塞 API 写。
Go 对比:
- Go 怎么做:测试里直接调返回的函数;goroutine 测试用 channel/
WaitGroup,无单独 async 关键字。 - Rust 为什么不同:
async fn产生 Future,必须有执行器驱动。 - Go 程序员易踩的坑:写了
async fn测试却不加 runtime,结果 Future 根本没跑。
记忆点:
- Async 测试是生态能力,不是
cargo test内建。 - 选 Tokio 就用
#[tokio::test](或等价block_on)。
Q13. 修完 bug 后怎样写一条有价值的回归测试?
Tags: common regression
适用版本: Rust 1.0+
一句话答案:
把当初触发 bug 的最小输入固化成 #[test],断言只锁住这次修复的行为;别塞进 main,也别顺手断言半个系统。
解答:
「❌」只在 main 里临时试跑——cargo test 不会当回归:
| |
「✅」用 #[test] 锁最小复现:
| |
| |
Go 对比:
- Go 怎么做:同样把事故输入做成
TestXxx/ 表驱动一行。 - Rust 为什么不同:额外常把边界压进类型系统,但行为回归仍要靠测试。
- Go 程序员易踩的坑:修复 PR 不带复现用例,几周后同样输入再破一次。
记忆点:
- 最小输入 + 相关断言 +
#[test]。 - 回归测试名字最好能读出“曾经坏在哪”。
Q14. 测试里为什么看不到 println!?dbg! 该怎么用?
Tags: common nocapture dbg!
适用版本: dbg! 需 Rust 1.32+;输出捕获为 Cargo 测试 harness 行为
一句话答案:
harness 默认捕获 stdout/stderr,通过的测试通常不展示打印;要实时看输出用 -- --nocapture。临时观察表达式优先 dbg!(带文件位置且返回原值)。
解答:
| |
| |
失败时 harness 常会把捕获到的输出一并展示,所以“先让断言失败”有时也能看到日志;但主动排查时 nocapture 更直接。
dbg! 是开发期探针,测完应删或改成正式断言;不要把 dbg! 留在库的非测试路径当日志系统。
Go 对比:
- Go 怎么做:
t.Log/t.Logf,配合go test -v才稳定可见。 - Rust 为什么不同:默认捕获所有 stdout,行为更“安静”。
- Go 程序员易踩的坑:以为没执行到
println!,其实只是被捕获了。
记忆点:
- 看不见打印 → 先加
-- --nocapture。 - 看中间值 →
dbg!;看完就删。
Q15. CI 里跑 cargo test 要注意什么?(--locked 等)
Tags: common CI locked
适用版本: Cargo
一句话答案:
CI 应用锁文件复现构建:cargo test --locked(或 --frozen);固定工具链;需要时分开 --lib / --doc;对共享状态测试考虑 --test-threads=1;别在 CI 默默 cargo update。
解答: 常见稳健命令组合:
| |
实务建议:
- 提交
Cargo.lock(对应用/bin 几乎总是;对纯库团队可另有约定,但 CI 仍应可复现)。 - 固定 rust 版本(
rust-toolchain.toml或 CI image 钉死 1.97.1 一类)。 - 缓存
target// registry 时,仍要用--locked防止“我机器能过、CI 解析出另一组依赖”。 - 文档测试、忽略测试:按需加
cargo test --doc、夜间-- --ignored。
Go 对比:
- Go 怎么做:
go test ./...,依赖go.sum;CI 常用go test -count=1关缓存。 - Rust 为什么不同:强调
Cargo.lock+--locked,和 feature / target 矩阵更复杂。 - Go 程序员易踩的坑:CI 不带
--locked,某天传递依赖静默升级导致红。
记忆点:
- CI:
cargo test --locked(或--frozen)。 - 钉工具链 + 可复现依赖 = 少见鬼。
Q16. 二进制项目(有 main)的测试写在哪?
Tags: common bin lib
适用版本: Rust 1.0+
一句话答案:
逻辑尽量放进 src/lib.rs(或模块),用 --lib 测;src/main.rs 只做薄封装。若坚持测 bin,可用 tests/ 调公共 API,或 #[cfg(test)] 写在可引用的模块里。
解答: 推荐结构:
| |
| |
| |
(上例仅示意“main 调用 lib”;真正工程里把 my_app 换成你的包名。)
只把代码堆在 main.rs 且不拆 lib 时,单元测试与复用都会别扭——这是 Go 里 “把逻辑放出 main 包” 的同一类建议。
Go 对比:
- Go 怎么做:
package main可测,但大项目常拆内部包。 - Rust 为什么不同:
main二进制 crate 对集成测试的暴露方式与 lib 不同,先 lib 后 bin 最省心。 - Go 程序员易踩的坑:全部写在
main.rs,然后奇怪为什么不好写tests/。
记忆点:
- 可测逻辑进 lib;main 保持瘦。
Q17. 一个测试里失败了,怎样快速定位是断言还是 panic?
Tags: common failure backtrace
适用版本: Rust 1.0+
一句话答案:
断言失败通常带 assertion failed / left == right;未捕获 panic 是另一类。需要调用栈时设 RUST_BACKTRACE=1(或 full)再跑该测试。
解答:
| |
| |
RUST_BACKTRACE 只影响 panic 报告;普通 Result 错误返回不会因此多出栈。测试失败本质常是断言宏内部 panic!,所以 backtrace 对深嵌套辅助函数很有用。
Go 对比:
- Go 怎么做:失败默认带文件:行号;panic 自带栈。
- Rust 为什么不同:默认 panic 报告更短,要栈就开环境变量。
- Go 程序员易踩的坑:只看最后一行 panic 文案,不打开 backtrace,找不到真正调用点。
记忆点:
- 先缩到单个测试 +
nocapture。 - panic / 断言搞不清时开
RUST_BACKTRACE=1。
Q18. 测试之间如何共享夹具?有没有“全局 Setup”?
Tags: occasional fixture
适用版本: Rust 1.0+
一句话答案:
标准库没有 JUnit 式全局 @BeforeAll;用普通函数/模块当夹具,或在每个测试开头调用。跨测试共享可变全局态要非常谨慎,优先每个测试自建数据。
解答:
| |
需要临时目录等资源时,优先看 Q19;并行默认开启时,共享文件名/端口容易 flaky——要么隔离资源名,要么临时 -- --test-threads=1(见 Q4)。
Go 对比:
- Go 怎么做:
TestMain、子测试t.Run、或自行setup()。 - Rust 为什么不同:保持测试为普通函数 + 属性,夹具即语言层函数复用。
- Go 程序员易踩的坑:用
static mut/ 懒加载全局客户端且不加锁,并行下偶发失败。
记忆点:
- 夹具 = 函数;每个测试自建数据更稳。
- 共享可变全局是 flaky 重灾区。
Q19. 测试里写临时文件 / 临时目录怎么做?
Tags: common tempfile filesystem
适用版本: Rust 1.0+(标准库路径);tempfile crate 为第三方
一句话答案:
标准库可用 std::env::temp_dir() + 唯一子路径,测完再 remove_file / remove_dir_all;也可以自制极简 TempDir(Drop 里清理)。生产测试更常把 tempfile 写进 [dev-dependencies](本篇用 text 点到为止)。
解答:
最小自制临时目录(可编译):
| |
更短的“只借系统临时目录拼唯一文件”写法:
| |
注意点:
- 路径要唯一(时间戳、进程 id、随机串),否则并行测试抢同一路径会 flaky(见 Q4、Q18)。
- panic 时只要
TempDir还在栈上,Drop仍会跑;手动temp_dir()拼路径却忘了清理,容易在 CI 机器上堆垃圾。 - 只测纯逻辑时尽量别碰真实磁盘;非碰不可再上临时目录。
第三方 crate(text,避免本仓强绑版本):
| |
Go 对比:
| |
- Go 怎么做:
t.TempDir()/os.CreateTemp很省事。 - Rust 为什么不同:标准测试框架不内置等价 API,常见是自制 RAII 或
tempfile。 - Go 程序员易踩的坑:固定写死
/tmp/mytest或仓库内相对路径,并行一开就互相踩。
记忆点:
- std:
temp_dir+ 唯一名 +Drop/显式清理。 - 省事:
tempfile作 dev-dependency。 - 并行下路径必须隔离。
Q20. 怎么 mock 依赖?(trait + 测试替身;mockall 用 text 一笔)
Tags: common mock trait test-double
适用版本: Rust 1.0+(手写替身);mockall 为可选第三方
一句话答案:
把依赖收成 trait,生产代码依赖 impl Trait / 泛型;测试里给一个假实现(test double / 测试替身)。需要录制调用次数、按参数返回时,再用 mockall 一类库生成 mock——多数项目手写假类型就够。
解答: 手写替身(推荐默认路径):
| |
只在测试里需要假类型时,可把假实现放进 #[cfg(test)] 模块,避免污染正式 API。动态派发也行:Box<dyn Clock>,测起来同样塞假对象。
需要「断言被调用了几次 / 按参数返回」时再用生成库(text 一笔,不绑死版本):
| |
别 mock 一切:纯函数、本地可造的数据(见 Q10 表驱动)优先真输入;I/O、时间、远端服务才值得替身。异步依赖同理——trait 方法返回 Future / 用 async trait,测试里给立即完成的假实现(异步测试见 Q12)。
Go 对比:
| |
- Go 怎么做:interface + fake / gomock。
- Rust 为什么不同:同样靠 trait 边界;无全局 monkey-patch 常规路径。
- Go 程序员易踩的坑:找「给任意函数打桩」的框架——Rust 更偏向设计可替换的 trait。
记忆点:
- 先 trait + 手写假实现。
- 编排复杂再
mockall;能表驱动就别 mock。
Q21. proptest 属性测试最小怎么写?(对标 quickcheck)
Tags: hot proptest property-based quickcheck
适用版本: proptest 1.x(以 crates.io 为准)
一句话答案: 用 proptest 随机生成输入,断言「对任意合法输入都成立」的不变量——比手写几组表驱动更能挖边界。对标 Go 生态的 quickcheck 一类,不是替代单元测试,而是补强。
解答:
| |
| |
| |
失败时 proptest 会尝试「缩小」输入到更小反例,便于修。和 Q10 表驱动搭配:表驱动锁已知案例,proptest 扫未知空间。
Go 对比:
- Go 标准库无内建属性测试;社区有 quickcheck 移植。
- Rust 为什么不同:
proptest!宏是社区事实标准之一。 - Go 程序员易踩的坑:用属性测试测「实现细节顺序」——应测不变量。
记忆点:
- 不变量 + 随机输入 → proptest。
- 与表驱动互补,不互斥。
Q22. 集成测试怎么 mock HTTP 上游?(wiremock / 假服务)
Tags: hot wiremock httptest HTTP mock
适用版本: wiremock 等;以文档为准
一句话答案:
测「调用下游 HTTP」时,在测试里起一个 假 HTTP 服务(常见 wiremock),把客户端 base URL 指过去;不要打真实外网。对标 Go 里自建 httptest 服务器或 mock。
解答:
| |
| |
| |
测自己的 axum handler 用 Tower oneshot(见 52-http Q10);测依赖的上游用 wiremock。
Go 对比:
| |
- Go 怎么做:
httptest.NewServer很常见。 - Rust 为什么不同:crate 名不同,模式相同。
- Go 程序员易踩的坑:集成测试直连生产 URL。
记忆点:
- 上游假服务 → wiremock。
- 本服务 handler →
oneshot(52)。
Q23. CLI 二进制怎么做黑盒测试?(assert_cmd / 快照)
Tags: common assert_cmd CLI insta snapshot
适用版本: assert_cmd;可选 insta
一句话答案:
用 assert_cmd 找到并运行 CARGO_BIN_EXE_<name>,断言退出码与 stdout/stderr。输出易变时再用 insta 快照。对标「把编好的 CLI 当黑盒跑」。
解答:
| |
| |
| |
帮助文本、子命令路由、非法参数退出码(见 38-cli)特别适合这条路径。
Go 对比:
| |
- 心智相同。
- Go 程序员易踩的坑:只测库函数、从不跑真正二进制。
记忆点:
Command::cargo_bin+ 断言退出码/输出。- 大段输出 → insta 快照。