- 开发工具
- 文档
【免费下载链接】mdBook
Create book from markdown files. Like Gitbook but implemented in Rust
导读
本指南以 mdBook 仓库的集成测试套件文档 tests/testsuite/README.md 为主体,深入讲解 mdBook 官方测试体系的核心组件:负责搭建临时书籍环境的BookTest驱动类,以及基于snapbox的快照断言机制。读完本文,你将掌握如何仿照官方测试组织方式,为 mdBook 的功能模块编写可链式调用、可自动更新期望值、且能校验 CLI 控制台输出的集成测试,并能直接对照 tests/testsuite/book_test.rs 与 tests/testsuite/main.rs 的源码理解底层实现。
一、测试套件概览:面向全功能的集成测试主阵地
tests/testsuite是 mdBook 的主测试套件,用于全面锻炼(exercise)mdBook 的所有功能。其入口文件 tests/testsuite/main.rs 的模块声明清晰展示了按功能切分的组织原则:
mod book_test; mod build; mod cli; mod config; mod includes; mod index; mod init; mod markdown; mod playground; mod preprocessor; mod print; mod redirects; mod renderer; mod rendering; #[cfg(feature = "search")] mod search; mod test; mod theme; mod toc;从源码结构可以看到两条明确的设计约定:
- 按功能模块组织:
build(构建)、config(配置解析)、markdown(Markdown 渲染)、preprocessor(预处理器)、renderer(渲染器)、theme(主题)、toc(目录)等各自独立成文件,每个文件内再以#[test]函数承载具体用例。新增测试时应遵循同样的粒度,而非把所有用例塞进一个大文件。 - 共享 prelude:
main.rs中定义了一个prelude模块,统一导出BookTest、glob_one、read_to_string与snapbox::str,各测试模块通过use crate::prelude::*;一行引入即可,避免重复导入。
测试统一由BookTest驱动。它会自动创建一个临时目录作为测试工作区,并提供一系列方法帮助构建书籍、修改文件、校验输出。这意味着每个测试都拥有一个干净、隔离、可复现的书籍环境,不会污染真实目录。
二、测试的基本结构:复制目录 → 运行命令 → 断言输出
BookTest的典型用法是:把一个预先写好的书籍源码目录复制进临时目录,然后在临时目录中运行 mdbook 命令。你可以选择以下两种驱动方式:
- 运行
mdbook可执行文件(通过BookTest::run):最大好处是能直接校验控制台输出(stdout/stderr),覆盖日志、错误信息等面向用户的输出行为; - 直接调用 mdBook API(通过
BookTest::load_book拿到MDBook实例后调用build()等方法):适合做纯逻辑层面的操作,例如注入自定义渲染器后触发构建。
最朴素的示例是 tests/testsuite/build.rs 中的build::basic_build:
// Simple smoke test that building works. #[test] fn basic_build() { BookTest::from_dir("build/basic_build").run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }); }这个用例验证了"构建一本书"这条主链路:从tests/testsuite/build/basic_build复制书籍源码(其 book.toml 只声明了[book] title = "basic_build"),执行mdbook build,并断言标准错误输出中包含三条日志。注意其中[ROOT]是一个占位符——它会被替换为实际的临时目录路径(详见下文 Snapbox 的 redaction 机制)。
同文件中还可以看到对失败路径的测试风格:failure_on_missing_file 使用cmd.expect_failure()断言chapter_1.md缺失时构建失败,并用[TAB]、[NOT_FOUND]等占位符屏蔽平台差异。
链式调用:一个测试串联多个动作
BookTest被设计为支持链式调用(chaining),所有变更方法返回&mut Self,可以一气呵成地表达"修改 → 重建 → 校验"的完整流程。原文档给出的骨架如下:
BookTest::from_dir("theme/mytest") .build() .check_main_file("book/index.html", str![["file contents"]]) .change_file("src/index.md", "new contents") .build() .check_main_file("book/index.html", str![["new contents"]]);执行流程为:从theme/mytest复制书籍 → 构建 → 校验book/index.html的<main>区域内容 → 修改src/index.md→ 重新构建 → 再次校验内容已更新。check_main_file只比较<main>标签之间的内容,因为模板外壳(导航、页头等)通常不是测试关注点,且会引入大量噪音。
三、动手编写一个主题测试:从目录创建到用例落地
原文档以"新建一个主题测试"为例,给出了一套可复制的实操流程,这里结合仓库现状逐步展开:
- 创建书籍源码目录:在
tests/testsuite/theme下新建目录(例如theme/mytest),放入想要测试的书籍源码。最低要求是src/SUMMARY.md,通常还需要book.toml来配置主题相关选项(如[output.html] theme = "..."等)。 - 添加测试函数:在 tests/testsuite/theme.rs 中新增一个
#[test]函数,以BookTest::from_dir("theme/mytest")起步,再调用需要的动作方法完成验证。
仓库中已有的主题测试可作参考,例如 theme.rs 中的 missing_theme 验证"主题目录不存在时报错",override_index 验证自定义index.hbs是否生效:
// Checks overriding index.hbs. #[test] fn override_index() { BookTest::from_dir("theme/override_index").check_file( "book/index.html", str![[r#" This is a modified index.hbs! "#]], ); }注意这里的from_dir路径是相对于tests/testsuite目录的(源码中Path::new("tests/testsuite").join(dir),见 book_test.rs),因此写"theme/mytest"即指tests/testsuite/theme/mytest。若目录不存在,from_dir会直接 panic 提示{dir:?} should exist,帮助快速定位路径拼写错误。
三个创建入口的选择
BookTest提供三种构造方式(见 book_test.rs):
| 构造方式 | 适用场景 | 特点 |
|---|---|---|
BookTest::from_dir(dir) | 绝大多数测试 | 复制tests/testsuite/<dir>到临时目录,expected子目录可用于check_all_main_files |
BookTest::empty() | CLI 行为测试(如cli.rs的 no_args/help) | 空临时目录,无书籍源码 |
BookTest::init(f) | 需要程序化初始化书籍 | 通过&mut BookBuilder回调配置并构建初始书籍,不复制任何目录 |
例如BookTest::init(|bb| { bb.copy_theme(true); })用于测试主题初始化时字体文件的复制行为(见 theme.rs 的 theme_fonts_copied)。
四、Snapbox:快照断言与自动更新期望值
测试套件的大部分断言由snapbox库驱动(对应原文档的 Snapbox 一节)。它提供多种字符串比较方式,期望内容以两种宏写在源码中:
str!:期望值内联在源码字符串中,如str![[r#"..."#]];file!:期望值存放在独立文件中,如file!["cli/no_args.term.svg"]、file。.term.svg后缀是 snapbox 对终端输出快照的约定命名。
SNAPSHOTS=overwrite:一键刷新期望值
这是快照工作流的"魔法"所在:设置环境变量
SNAPSHOTS=overwrite cargo test后,snapbox 会自动把实际输出写回str!中的字符串,或覆盖file!指向的文件内容,从而省去手工抄写期望值的繁琐。原文档推荐的实践是:
- 写测试时先放一个空的
str!或file!; - 运行一次带
SNAPSHOTS=overwrite的测试让 snapbox 自动填充; - 仔细审查填充进来的内容,确认与预期一致后再提交。
这个"生成后人工复核"的步骤非常关键——快照自动更新是把双刃剑,若不加审查,可能把真实的 bug 也一并"固化"进期望值。
通配符与过滤器
期望内容支持两类通配符:
...:匹配任意行(含多行内容);[..]:匹配同一行上的任意字符。
例如check_file_contains内部正是把目标字符串包装成"...\n[..]{expected}[..]\n...\n"再与整个文件比较(见 book_test.rs),实现"文件中某处包含指定内容"的语义。str!中[[ ]]括号内还可以额外传入格式化参数(如str![["{title}"], title = "x"]),进一步提升可读性。
snapbox 还提供了丰富的其他过滤器与 diff 输出(原文档指向 snapbox 官方文档了解全量能力)。结合仓库源码,可以确认测试套件在 book_test.rs 的 assert 函数 中注册了一套自动化替换规则(redactions),这正是原文档所说"应用于字符串的规范化":
| 占位符 | 被替换的内容 | 目的 |
|---|---|---|
[ROOT] | 测试临时目录的绝对路径 | 屏蔽机器相关的路径差异 |
[VERSION] | mdbook_core::MDBOOK_VERSION | 屏蔽版本号差异 |
[NOT_FOUND] | 各平台的文件不存在错误文本(Unix/Windows 消息、program not found) | 跨平台兼容 |
[EXIT_STATUS] | 各平台的退出状态措辞(exit status/exit code) | 跨平台兼容 |
[TAB] | 制表符\t | 消除行内空白差异 |
[EXE] | 平台可执行文件后缀(如 Windows 的.exe) | 跨平台兼容 |
这就是为何basic_build的期望值里能直接写[ROOT]/book而不是某个具体临时路径,也是跨平台 CI 能共享同一份快照的底层保障。
五、BookTest 方法全景:常用的验证与操作手段
原文档建议"reviewing the methods onBookTest"来熟悉能力边界,这里结合 book_test.rs 源码将常用方法整理为两类:
断言/校验类方法
| 方法 | 行为 | 典型用途 |
|---|---|---|
check_main_file(path, expected) | 提取 HTML 文件中<main>...</main>之间的内容并断言(自动先构建) | 验证章节正文渲染结果,忽略模板噪音 |
check_all_main_files() | 遍历构建产物中所有.html(排除index.html/404.html/print.html/toc.html),逐个与书籍源码目录下expected/中同名文件比对,并检查没有多余的 expected 文件 | 全量回归渲染结果,如 rendering.rs 的 fontawesome 测试 |
check_file(path_pattern, expected) | 断言某个文件的完整内容,路径支持 glob 通配符但必须只匹配一个文件 | 校验非 HTML 文件(如配置产物) |
check_file_contains(path_pattern, s) | 断言文件某处包含指定字符串 | 检查edit-url-template链接等局部特征,见 rendering.rs |
check_file_doesnt_contain(path_pattern, s) | 断言文件不包含指定字符串(文档提示该方法较脆弱,可能漏检回归,应谨慎使用) | 反向特征校验 |
check_file_list(path, expected) | 递归列出某目录下所有文件的相对路径(排序后逐行比较) | 验证构建产物文件清单,如默认字体集合 |
check_toc_js(expected) | 从book/toc*.js中提取innerHTML内容并断言 | 验证目录(TOC)的生成结果 |
操作类方法
| 方法 | 行为 | 典型用途 |
|---|---|---|
build() | 从临时目录加载MDBook并构建 | 显式触发构建 |
load_book() | 返回MDBook实例 | 调用 mdBook API 进行深度操作 |
run(args, f) | 在临时目录运行mdbook可执行文件,参数按空白切分(含引号会 panic),回调里用BookCommand定制断言 | 校验 CLI 输出与退出码 |
change_file(path, body) | 覆盖写入临时目录中的文件 | 模拟用户编辑源码后重新构建 |
rm_r(path) | 删除临时目录中的文件或目录 | 模拟缺失文件/目录的场景 |
rust_program(path, src) | 用rustc在临时目录编译一段 Rust 源码生成可执行文件 | 构造自定义渲染器/预处理器的可执行程序,见 renderer.rs 的 failing_command |
六、BookCommand:对 mdbook 可执行文件的精细控制
BookTest::run的回调参数是一个BookCommand(定义于 book_test.rs),它封装了对mdbook进程的完整控制面:
- 退出码断言:默认期望成功;
expect_failure()期望非零退出;expect_code(2)精确断言指定退出码(如cli.rs中无参数调用时期望退出码 2)。 - 输出断言:
expect_stdout(...)/expect_stderr(...)接受str!或file!数据,通过snapbox::Assert的try_eq比对并渲染友好 diff。 - 参数与环境:
args(&["--dest-dir", "foo", ".."])追加命令行参数(空格无法用run的字符串参数表达时使用);env(key, value)设置进程环境变量;current_dir(path)改变运行目录(如 build.rs 的 dest_dir_relative_path 在work子目录中运行并断言--dest-dir foo生效)。 - 调试:
debug("mdbook=trace")传入MDBOOK_LOG值,打印实际执行的命令及其输出,但在 CI 环境下会直接 panic,防止调试代码泄漏到流水线。 - 环境隔离:
run()内部会移除MDBOOK_LOG、屏蔽系统 git 配置(GIT_CONFIG_NOSYSTEM=1并把全局/系统 git 配置指向临时目录)、移除GIT_AUTHOR_*/GIT_COMMITTER_*等环境变量,确保测试不受外部环境干扰(见 book_test.rs)。
BookCommand的能力在 CLI 与配置测试中大量使用:例如 config.rs 通过cmd.env("MDBOOK_BOOK__TITLE", "Custom env title")验证环境变量配置覆盖能力;markdown.rs 用cmd.env("MDBOOK_OUTPUT__HTML__SMART_PUNCTUATION", "false")验证开关配置。这些用例共同印证了"运行可执行文件可以验证控制台输出"这一设计初衷。
七、运行测试
测试入口在 tests/testsuite/main.rs(#![allow(unreachable_pub)]表明这是集成测试 crate),标准的 Rust 工作区测试方式即可运行,例如:
# 运行整个测试套件 cargo test --test testsuite # 运行单个测试(如 basic_build) cargo test --test testsuite basic_build # 覆盖并自动更新所有快照 SNAPSHOTS=overwrite cargo test --test testsuite注意部分测试受 feature 开关影响:search模块需要feature = "search"才会编译(见 main.rs);cli.rs的no_args/help用例在未启用watch与serve特性时会被#[cfg_attr(..., ignore)]跳过(见 cli.rs)。因此跑全量测试时建议开启完整特性集,否则会有用例被静默忽略。
八、总结:测试套件的设计要点回顾
回顾 tests/testsuite/README.md 与配套源码,可以提炼出 mdBook 集成测试的四个核心设计原则:
- 按功能分模块:每个大功能(build、config、theme、renderer…)拥有独立测试文件,书籍夹具按主题存放在
tests/testsuite/<feature>/<case>目录; - BookTest 统一驱动:临时目录 + 链式方法调用,让"准备 → 构建 → 修改 → 再构建 → 断言"的流程可读且可组合;
- 快照断言 + 自动更新:
str!/file!定义期望,SNAPSHOTS=overwrite自动回填,占位符 redaction 保证跨平台一致性; - 双通道验证:既能跑
mdbook可执行文件校验真实 CLI 输出(含退出码、stdout/stderr、环境变量),也能直接调用 API 做细粒度控制,兼顾黑盒与白盒测试。
对于想要为 mdBook 贡献测试的开发者,最直接的路径是:参考 theme.rs 或 build.rs 的既有用例,在对应功能目录下新建书籍夹具,然后套用BookTest::from_dir(...).build().check_xxx(...)的链式模板,最后用SNAPSHOTS=overwrite生成并审查快照——这套工作流同样适用于任何以 snapbox 为基础快照框架的 Rust 项目。
- 开发工具
- 文档
【免费下载链接】mdBook
Create book from markdown files. Like Gitbook but implemented in Rust
相关推荐
Camoufox 测试体系实战指南:基于 Playwright 一致性套件与补丁验证测试
Camoufox 测试体系实战指南:基于 Playwright 一致性套件与补丁验证测试 Camoufox 在 tests/ https://link.gitc
网页爬虫浏览器控制Reason 仓库 refmt cram 测试套件实战指南:dune 快照测试的运行、快照更新与多版本 OCaml 验证
Reason 仓库 refmt cram 测试套件实战指南:dune 快照测试的运行、快照更新与多版本 OCaml 验证 本文是 Reason 官方仓库 tes
编程语言编译器connectedhomeip 单元测试实战指南:基于 pw_unit_test 编写、构建与调试 Matter SDK 测试套件
connectedhomeip 单元测试实战指南:基于 pw_unit_test 编写、构建与调试 Matter SDK 测试套件 导读 本文以 connect
物联网智能家居嵌入式通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考