编程语言研究书库

第 6 章拆解 — 测试与质量保证:从单元测试到测试金字塔

测试与质量保证:从单元测试到测试金字塔

这一章还第 3 章欠下的测试债。 书里开头就认错:真实项目里测试应该和代码 同时开始写(那叫 TDD),这本书故意把顺序倒了,「先把酷的东西秀出来再干杂活」—— 但「规格应当先于实现,或紧随其后,绝不能拖太久」1。 六种测试形态逐个给一条能跑的路,最后用「测试金字塔」告诉你每种该写多少。

1. 顶层全景

  后端 ← 单元测试(同文件、#[test]、隔离 + mock)
       ← 集成测试(tests/ 目录、内存库、边界思维)
  前端 ← Wasm 测试(wasm-bindgen-test、wasm-pack、真浏览器/Node)
       ← Selenium(thirtyfour 驱动真浏览器)
  协作 ← BDD(人读的规格书 + 代码绑定,Cucumber)
       ← Playwright(治 flaky;Rust 绑定老了就用官方语言)
  收口 ← 测试金字塔:便宜的多写,贵的少写,都要常跑

  图说:按「离用户多近」排,越往上越像用户、越贵、越少。

2. 前后端测试的不同

后端是应用逻辑 + 暴露端点的 API;前端是渲染 GUI + 在背后跟后端说话。 「点击这个按钮会发生什么」在后端根本无从问起——视角不同,测什么、怎么测都不同; 但 Given-When-Then 这类概念两端通用2。

3. 单元测试:小就是美

定义:单元测试把一块功能从所有环境里剥出来,只验它自己的逻辑——越小越好3。 Rust 里它和实现写在同一个文件,用 #[cfg(test)] mod tests 包起来: 测试代码照常编译,但普通构建会把它们排除;use super::* 拿到被测模块4。

书里拿项目截止日校验器开刀,两个测试一对:

#[test]
fn test_deadline_validation_future() {
    let deadline = Utc::now().checked_add_days(Days::new(2));
    assert!(ProjectValidation::deadline_validation(&deadline).is_ok(),
            "Deadline should be valid in the future.");
}

#[test]
fn test_deadline_validation_expired() {
    let deadline = Some(Utc::now());
    assert!(ProjectValidation::deadline_validation(&deadline).is_err(),
            "Deadline should be invalid if expired.");
}

Given-When-Then 三段式(术语来自 BDD):Given 是前提(构造数据), When 是触发(调用被测函数),Then 是断言(assert!)。 assert! 把 When 和 Then 揉在了一起,但更复杂的测试里尽量保持三段分离5。

两个实用贴士(书里原话):

  • 断言消息里用 should/must/have to,别用 is/are——后者分不清 「当前行为」还是「期望行为」(原书脚注的例子很妙:引擎在电路熔断时停机 是期望行为,在换挡时停机是不可接受的——「引擎必须继续转」就没有歧义)6;
  • 单元测试要隔离,依赖得换 mock(假替身:不跑真逻辑、直接返回预期输出; SeaORM 自带 mock 可替换数据库)7。

cargo test 跑起来,报告两个测试 ok、0 失败;doc-test 也提了一笔: 特殊格式的文档注释可以直接当测试跑,文档和测试从此同步8。

4. 集成测试:沿边界测

定义:集成测试放弃隔离,专门测「组件拼在一起玩得好不好」——典型边界是 数据库连接和外部 API。边界可以很浅(一次方法调用),也可以很深(网络层); 测的是「这条边界传达的消息和我们想的一模一样」9。

Rust 的约定:tests/ 目录,cargo test 自动发现,不用配置10。 CI 上没有真数据库——用内存 SQLite(sqlite::memory:)顶替: 立得快、跑得快的轻量替身;为此加 [dev-dependencies](只测试用,不进生产; 但书里提醒:不同库之间有方言差异,重特性可能不兼容,马上就会看到)11。

测试文件三段结构:12

  1. 异步包装: Rust 的 #[test] 是同步的,而代码是异步的——手搭 Tokio runtime rt.block_on(future) 当桥(书里给了简法:#[tokio::test] 一步到位)13;
  2. 环境准备: common/mod.rs 里放 setup_schema(建表); 因为主库是 MySQL、测试库是 SQLite,方言不兼容,建表语句只能为测试重写一份 ——「造成了一点重复,但也展示了 SeaORM 的灵活性」14;
  3. 测试用例: 插入 Alice(ActiveModel + Set),断言 last_insert_id == 1。断言只有一处没关系——过程中任何地方 panic 都算测试失败15。

5. Wasm 前端测试:换个运行时

解决什么问题: 前端编译成 Wasm、跑在浏览器/Node 里,普通的 cargo test 够不着。 救星是 wasm-bindgen-test:替换默认的 #[test] 宏;执行器是 wasm-pack (类似 webpack 的 Wasm 打包器),可以 --node 跑在 Node 里, 或 --chrome/--firefox/--safari 跑在真浏览器里(配好 WebDriver), --headless 不开 GUI16。

依赖三件套(都是 dev-dependencies):gloo-utils(取 window/document 之类), js-sys(全局 JS API),wasm-bindgen-test(测试宏)17。

两个规矩:测试必须在 crate 根或公开模块里,否则找不到;不用 cargo test 跑, 用 wasm-pack test。想继续用 cargo,可以在 .cargo/config 里给 wasm32 目标配 runner = wasm-bindgen-test-runner,然后 cargo test --target wasm32-unknown-unknown18。

测试写法:渲染组件 yew::Renderer::<App>::new().render(), 用 gloo-utils 查 document 里的元素(get_elements_by_tag_name("input")); 异步测试靠 js-sys 的 Promise + JsFuture 转成 future 来 await19。

6. Selenium:真浏览器里的老标准

Selenium 是浏览器自动化测试的事实标准——但它跟 Rust 无关:Java 写的, 没有官方 Rust 支持。生态位由 thirtyfour(名字就是硒的原子序数 34)补上20。

门槛先说清:得先跑 Selenium Server(Java 包,java -jar selenium-server-<version>.jar standalone, 加 --selenium-manager true 自动管 WebDriver),它起在 4444 端口, 自带一个网页控制台21。

测试本体(WebDriver 测试天然异步):DesiredCapabilities::chrome() 定能力 → WebDriver::new("http://localhost:4444", caps) 连服务 → 开维基百科 → 找到搜索框 → send_keys("selenium") 模拟打字 → 点提交 → 等标题出现 → 断言 title() == "Selenium - Wikipedia" → driver.quit() 关浏览器22。

这个函数没挂 #[test]、还是异步的,所以照旧用 Tokio 的 new_multi_thread().block_on(...) 包一层同步壳——「跟 #[tokio::main] 干的一样, 只是这里必须显式来,因为测试函数本身不能是 async」23。 排错贴士也给了:对结果做模式匹配或 .expect("…"),让错误信息浮出来, 别被一堆无关输出淹没24。

7. BDD:先写给人看的规格

BDD(行为驱动开发)= 先定义期望行为 → 再实现「验证这个行为」→ 最后才实现功能本身。 核心载体是 feature 文件:用贴近自然语言的话描述行为,让全项目的利益相关者 说同一种语言,减少误会25。

Feature: Application Feature
    Scenario: If we run the main function, the application will start
        Given the main function has been called
        When I send a request
        Then a response will be received

Given/When/Then 三个关键词是骨架,不能改;其余自由发挥26。

接线的工具是 Cucumber(rust 版): Cargo.toml 加 dev-dependencies (cucumber、reqwest、thirtyfour、tokio)和 [[test]] name = "bdd_test" harness = false; Rust 侧三件套:27

  • World(「世界」):步骤之间搬运的载荷,比如存响应状态码;
  • 步骤函数: #[given("…")]、#[when("…")]、#[then("…")] 宏把函数绑到 feature 文件里的句子;
  • 入口: World::run("tests/features").await。

书里的例子有意选了个聪明的断言:请求一个不存在的端点,断言收到 404—— 「端点不存在」足以证明「应用活着且在服务」28。

一个细节暴露了真实世界的毛边:Given 步骤里启动应用 main 时不能直接 await (嵌套 runtime 会 panic),要 task::spawn 包一层;When 里还得 sleep(Duration::from_secs(2)) 等它起来——这个 2 秒正是下一节要治的病29。

书里对 BDD 的期望也说得克制:它不能保证没有误会(开发者仍可能偏离 feature 文件的 措辞),但至少 feature 文件就住在项目里,偏差会被持续看见30。

8. Playwright:专治 flaky 的务实选择

先解释词。 flaky test = 不是真有错却随机失败的测试(比如那个 sleep 2 秒, 机器一慢就误判)。没人想要 flaky 测试——「它们招人烦;开发者和 QA 都想看到 只在真出错时才红的测试」31。

Playwright 的承诺:跨平台、跨浏览器、跨语言的统一 API,完全隔离、可信事件 (连 hover 都管),自动等待元素出现而不是让你 sleep。但它同样不是 Rust 专属 ——Rust 甚至没有官方绑定,crates.io 上的社区绑定 2022 年后就没更新过。

书里在这里做了个少见的务实让步: 如果可以用 TypeScript 或 Python 写 Playwright 测试,就直接用官方语言——「那样测试代码最干净」; 为了「全书都用 Rust」的叙事才勉强给了 Rust 示例,并且明说不指望这个 crate 活太久32。演示照走:initialize(自动装驱动)→ prepare(下载浏览器, 一次性)→ chromium headless → goto → eval 跑 JS 并 serde 反序列化结果 → click → 等 2 秒 → 取 title 断言33。

判断(我们的,不是书里的): 这一节是全章最有分寸感的地方——作者连续在两处 「为了叙事完整性」用了次优方案(Playwright 的 Rust 绑定、Selenium 的手搭 runtime), 而且都当场标注了更好的做法。「工具忠于问题,不忠于叙事」是这一章真正的教训。 如果错,会错在: 到 2026 年,Playwright 官方已有更多语言绑定、Rust 生态的 测试工具链也比写书时成熟,这份「务实清单」需要按新现状重查再照抄。

9. 测试金字塔:每种写多少

五个排序维度: 完整性(隔离↔全集成)、速度、搭建成本、范围、暴露面 (用户可见接口 ↔ 内部实现准确性)。规律:越想测得透,代价越大—— 底层便宜测试测不到终端体验,顶层彻底测试跑的是生产形态,实现和运行都贵34。

        ╱ E2E/UI 测试(Selenium/Playwright)╲      ← 少:贵、慢、要环境
       ╱   集成测试(数据库/服务边界)      ╲     ← 中
      ╱     单元测试(纯逻辑,成百上千)      ╲    ← 多:便宜、秒级、随便跑

  图说:底部多写、顶部少写;写是一回事,「还得经常跑」是另一回事——
  越贵的测试越跑不起,所以更要省着用。

书里引了 Martin Fowler 网站上 Ham Vocke 的那篇测试金字塔文章作为延伸读物35。

10. 作者的判断与证据(分开标)

有出处的: #[test]/#[cfg(test)]、tests/ 目录约定、wasm-bindgen-test 的 运行规矩,都是工具链事实;金字塔引 Fowler 站文章;BDD 的 Given-When-Then 词源注明出自 BDD36。

作者自己的评估:

  • 「单元测试是最重要的 QA 工具,要多写」+ TDD 的顺序主张1;
  • 「Rust Playwright 绑定命不久矣,用官方语言更好」的务实判断(见判断块);
  • 「flaky 测试人人喊打」是行业共识式的说法。

11. 边界与局限

  • 「写测试留给读者练习」: 本章明说了——只给方法、工具和小例子, 成体系的测试套件是练习(前言里就声明过)1。
  • Playwright Rust 绑定已停在 2022;Selenium 手搭 runtime 也非最佳实践——两处在文中均有交代。
  • Wasm 测试工具链「尚在开发中」: headless 浏览器矩阵、CI 集成都要自己拼装。
  • 没有讲性能测试/压测——与全书的风格一致(性能是选型论据,不是测试对象)。

12. 可带走的

  1. QA 从第一行代码开始; TDD 的顺序是「规格 → 测试 → 实现」, 最底线是「测试紧随其后,绝不拖太久」。
  2. 单元测试三件套: 同文件 #[cfg(test)]、Given-When-Then 结构、 断言消息用 should/must;依赖换 mock。
  3. 集成测试沿边界想: 内存 SQLite 当轻量替身;#[tokio::test] 省掉手搭 runtime; 方言差异要为测试重写建表——这是多数据库支持的实价。
  4. Wasm 测试: wasm-bindgen-test 宏 + wasm-pack 执行器,Node 或真浏览器, --headless 上 CI;测试要放 crate 根或公开模块。
  5. Selenium/thirtyfour: 4444 端口的 Selenium Server 先跑起来; 异步函数用 Tokio runtime 包同步壳。
  6. BDD 的 feature 文件让产品、开发、测试说同一种话;World 是步骤间的载荷; 「断言 404」证明应用活着,是低成本断言的范例。
  7. flaky 测试是团队的敌人; 等待用工具的内置等待,别 sleep 拍脑袋。
  8. 金字塔: 便宜的多写常跑,贵的少写慎用;测试写完不跑等于没写。

13. 原文地图

主题原书章原文位置
「测试拖太久了」的自白Quality Assurance and Testingtext/43-fm-introduction.txt:17(搜「test-driven development」)
前后端测试差异同上text/43-fm-introduction.txt:67(搜「click this button」)
单元测试定义与结构同上text/43-fm-introduction.txt:73(搜「isolate」)、:79(搜「cfg(test)」)
Given-When-Then同上text/43-fm-introduction.txt:158(搜「Given」)
断言消息措辞同上text/43-fm-introduction.txt:178(搜「must」)
mock 与 doc-test同上text/43-fm-introduction.txt:251(搜「mocks」)、:241(搜「doc-test」)
集成测试定义与边界思维同上text/43-fm-introduction.txt:262(搜「give up on the idea」)、:278(搜「along boundaries」)
tests/ 目录与内存 SQLite同上text/43-fm-introduction.txt:286(搜「tests folders」)、:289(搜「in-memory」)、:296(搜「dev-dependencies」)
方言不兼容重写建表同上text/43-fm-introduction.txt:385(搜「not exactly compatible」)
tokio::test 简法同上text/43-fm-introduction.txt:555(搜「tokio::test」)
Wasm 测试三依赖同上text/43-fm-introduction.txt:579(搜「gloo-utils」)、:597(搜「wasm-bindgen-test」)
wasm-pack 与 headless同上text/43-fm-introduction.txt:606(搜「wasm-pack test」)、:660(搜「headless」)
Selenium 与 thirtyfour同上text/43-fm-introduction.txt:697(搜「thirtyfour」)、:669(搜「selenium-server」)
WebDriver 测试全流程同上text/43-fm-introduction.txt:703(搜「test_with_webdriver」)、:763(搜「Selenium - Wikipedia」)
Tokio 包同步壳同上text/43-fm-introduction.txt:841(搜「same as what the tokio::main macro does」)
BDD 定义同上text/43-fm-introduction.txt:877(搜「expected behavior」)
feature 文件示例同上text/43-fm-introduction.txt:934(搜「Application Feature」)
World 与步骤绑定同上text/43-fm-introduction.txt:1027(搜「World」)、:1039(搜「given」)
断言 404 的智慧同上text/43-fm-introduction.txt:1123(搜「good enough」)
spawn 与 sleep 两秒同上text/43-fm-introduction.txt:1120(搜「panic」)、:1066(搜「from_secs(2)」)
BDD 不保证无误会同上text/43-fm-introduction.txt:890(搜「continuously reminded」)
flaky 定义同上text/43-fm-introduction.txt:1198(搜「flaky」)
「用官方语言写 Playwright」同上text/43-fm-introduction.txt:1201(搜「last updated in 2022」)
Playwright 示例流程同上text/43-fm-introduction.txt:1225(搜「Playwright::initialize」)
测试金字塔五维度同上text/43-fm-introduction.txt:1352(搜「Integrity」)、:1371(搜「pyramid」)
「还得经常跑」同上text/43-fm-introduction.txt:1381(搜「on a regular basis」)
Points to remember同上text/43-fm-introduction.txt:1400(搜「QA starts」)

Footnotes

  1. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:17,搜「test-driven development」)。原文:「we would have started writing tests as soon as we started coding. This is called test-driven development (TDD).」以及「specification should precede the implementation or follow it as closely as possible, but by no means should it be postponed for too long」。 ↩ ↩2 ↩3

  2. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:67,搜「click this button」)。 ↩

  3. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:73,搜「isolate」)。 ↩

  4. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:79,搜「cfg(test)」;:145,搜「same file」)。 ↩

  5. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:158,搜「Given」)。 ↩

  6. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:178,搜「must」)。脚注 2 的引擎例子见同段脚注。 ↩

  7. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:251,搜「mocks」)。 ↩

  8. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:241,搜「doc-test」)。 ↩

  9. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:262,搜「give up on the idea」;:278,搜「along boundaries」)。 ↩

  10. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:286,搜「tests folders」)。 ↩

  11. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:289,搜「in-memory」;:296,搜「dev-dependencies」;:298,搜「incompatibilities」)。 ↩

  12. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:392,搜「Async wrapper」)。 ↩

  13. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:555,搜「tokio::test」)。 ↩

  14. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:385,搜「not exactly compatible」)。 ↩

  15. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:549,搜「panic anywhere」)。 ↩

  16. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:564,搜「no power there」;:606,搜「wasm-pack test」)。 ↩

  17. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:579,搜「gloo-utils」;:585,搜「js-sys」;:597,搜「wasm-bindgen-test」)。 ↩

  18. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:606,搜「root of the crate」)、:609(搜「runner」)。 ↩

  19. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:621,搜「Renderer」)、:624(搜「JsFuture」)。 ↩

  20. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:697,搜「thirtyfour」)。 ↩

  21. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:669,搜「selenium-server」)、:675(搜「4444」)。 ↩

  22. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:703,搜「test_with_webdriver」)、:763(搜「Selenium - Wikipedia」)。 ↩

  23. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:841,搜「same as what the tokio::main macro does」)。 ↩

  24. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:850,搜「extract the important message」)。 ↩

  25. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:877,搜「expected behavior」)、:880(搜「natural language」)。 ↩

  26. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:934,搜「Application Feature」)、:952(搜「keywords」)。 ↩

  27. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:904,搜「cucumber」)、:1024(搜「World」)、:1108(搜「run」)。 ↩

  28. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:1123,搜「good enough」)。 ↩

  29. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:1120,搜「would panic」)、:1066(搜「from_secs(2)」)。 ↩

  30. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:890,搜「continuously reminded」)。 ↩

  31. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:1198,搜「flaky」)。 ↩

  32. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:1201,搜「last updated in 2022」)。 ↩

  33. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:1225,搜「Playwright::initialize」)、:1333(搜「scarce」)。 ↩

  34. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:1348,搜「various scales」)、:1352(搜「Integrity」)、:1368(搜「the more thorough」)。 ↩

  35. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:1384,搜「Ham Vocke」)。 ↩

  36. 出处:「Quality Assurance and Testing」(text/43-fm-introduction.txt:1552,搜「behavior-driven development」)。 ↩

On this page