ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入剖析 tests_macros:用过程宏为 Rust 项目自动生成文件驱动测试

深入剖析 tests_macros:用过程宏为 Rust 项目自动生成文件驱动测试 深入剖析 tests_macros用过程宏为 Rust 项目自动生成文件驱动测试【免费下载链接】toolsUnified developer tools for JavaScript, TypeScript, and the web项目地址: https://gitcode.com/gh_mirrors/to/toolstests_macros 是 Rome面向 JavaScript、TypeScript 与 Web 的统一开发者工具链仓库中的一个小型过程宏 crate它提供了一组工具函数能够根据目录中的文件自动生成单元测试即文件驱动测试file-driven / snapshot testing。通过一条gen_tests!宏调用开发者即可让 glob 匹配到的每一个文件都被展开为一个独立的#[test]测试函数从而以极低成本建立庞大的测试矩阵。读完本文你将掌握该宏的安装方式、三个参数的完整语义、生成的测试代码形态、运行与过滤技巧并透过仓库源码理解其命名转换、目录嵌套、expected 文件配对等底层实现原理。一、它解决什么问题从手写测试到声明式测试在大型语言工具链项目中例如 Rome 的解析器、格式化器与 lint 分析器测试用例动辄成百上千每个语法特性、每个 lint 规则都需要独立的输入文件与期望输出。如果全部手写#[test]函数代码将极度重复且难以维护——新增一个用例必须同时修改测试源码。tests_macros::gen_tests!把这一过程变成纯声明式你只需要提供一个glob 模式匹配测试输入文件一个回调函数名负责真正执行断言逻辑一个文件类型标记透传给回调函数用于区分 module / script 等解析模式。宏会在编译期扫描文件系统为每个匹配到的文件展开出一个独立的测试函数并在展开时把输入文件完整路径与对应的 expected 文件完整路径作为字符串字面量注入测试函数体内。新增测试用例只需往目录里放一个文件然后重新运行cargo test无需再触碰任何测试源码。二、如何安装tests_macros 是一个proc-macro crate见 Cargo.toml 中的[lib] proc-macro true只能作为dev-dependencies使用。在任意 crate 的Cargo.toml中添加[dev-dependencies] tests_macros { path ../tests_macros }从源码结构看该 crate 对外只导出一个过程宏入口gen_tests见 lib.rs因此整个 API 就是一个宏、一条语句。三、宏的用法与三个参数宏的调用形式为tests_macros::gen_tests! {glob 模式, 回调函数路径, 文件类型标记}三个参数的含义如下参数类型含义第一个参数字符串字面量传递给 globwalk 库的 glob 模式基准目录是该 crate 的Cargo.toml所在目录即CARGO_MANIFEST_DIR模式格式遵循 gitignore 的模式语法如**、*、{a,b}、?等第二个参数路径一个将被调用、接收每个文件的完整路径等方法参数的回调函数路径可以带模块限定如crate::run_test第三个参数字符串字面量文件类型标记如module、script会作为字符串原样传给回调函数从实现看解析逻辑在 lib.rs 的 Arguments::parse依次解析字符串字面量glob、逗号、函数路径、逗号、字符串字面量file_type其中 glob 与 file_type 都只接受字符串字面量不支持变量或表达式。3.1 推荐用法把宏放进一个模块原文档建议将宏放在一个模块内部这样既能把生成的大量测试函数隔离在命名空间里又能利用模块名对测试进行批量过滤。最小示例mod some_mod { tests_macros::gen_tests! {tests/*.{js,json}, run_test} // input_file 和 expected_file 都是完整路径 fn run_test(input_file: str, expected_file: str) { println!({:?} {:?}, input_file, expected_file); } }注意实际生成的测试代码会向回调函数传递4 个参数——test_file、test_expected_file、test_directory、file_type详见下文。上面的run_test签名是原文档中的简化示例真实使用时请按 4 参数签名编写回调。3.2 每个文件都会生成什么对于匹配到的每一个文件例如tests/sometest.txt宏大约会展开为如下形态#[test] pub fn somefilename() { let test_file crates cargo.toml 所在目录/tests/sometest.txt; let test_expected_file crates cargo.toml 所在目录/tests/sometest.expected.txt; let file_type module; let test_directory crates cargo.toml 所在目录/tests; run_test(test_file, test_expected_file, test_directory, file_type); }关键点测试函数名是文件名的 snake_case 版本如SomeFile.js→some_file_jsexpected 文件的推导规则为与输入文件同目录、同文件名扩展名前插入.expected如sometest.txt→sometest.expected.txt这一逻辑在 Arguments::get_variables 中实现代码生成使用quote!宏拼接 token并为测试函数注入合适的调用点 span便于编译器报错时定位到宏调用处见 lib.rs 的 gen 方法。四、源码级原理展开过程逐段拆解gen_tests的整体执行流程可以从 lib.rs 梳理出来4.1 收集文件globwalk 过滤Arguments::get_all_fileslib.rs#L156-L168读取CARGO_MANIFEST_DIR环境变量作为基准目录用GlobWalkerBuilder::new(base, glob)构建遍历器。AllFiles迭代器lib.rs#L29-L57在遍历时做了两层过滤跳过文件名中包含expected的文件避免把.expected.*期望输出文件也当成测试输入只保留元数据为普通文件meta.is_file()的条目跳过目录。4.2 生成变量命名、路径与 expected 配对Arguments::get_variableslib.rs#L170-L202针对每个文件计算四元组test_namefile_stem.to_snake()后再拼接扩展名如foo-bar.tsx→foo_bar_tsxtest_full_path/test_expected_fullpath输入文件与 expected 文件的完整路径test_directory输入文件所在目录。4.3 命名转换transform_file_name函数名必须同时满足合法 Rust 标识符与可读性要求。transform_file_namelib.rs#L59-L115做了三件事将-、.、、等字符替换为下划线将大写字母转为_ 小写camelCase 文件名也能得到可读的 snake_case 测试名处理两类合法性问题若转换结果是 Rust 关键字await、for、return、type、enum等一长串列表则追加_后缀若以数字开头则在前面加_前缀。4.4 按目录自动生成嵌套模块这是该宏的亮点之一Modules结构lib.rs#L117-L153会根据测试文件路径中specs之后的目录层级把测试组织进多层嵌套的mod中使得cargo test -p some-crate -- module_a::module_b::test_name这样的细粒度过滤成为可能。路径切片逻辑见 gen 方法中的组件遍历它会从文件路径中skip_while(|item| *item ! specs)即只取specs目录之后的相对层级来构造模块名。五、仓库中的真实使用案例tests_macros 在 Rome 各分析器、格式化器的测试基建中扮演了核心角色以下都是仓库中真实存在的调用每个调用的第三个参数即为文件类型标记rome_js_analyzelint 规则测试在 tests/spec_tests.rs 中声明了两组测试tests_macros::gen_tests! {tests/specs/**/*.{cjs,js,jsx,tsx,ts,json,jsonc}, crate::run_test, module} tests_macros::gen_tests! {tests/suppression/**/*.{cjs,js,jsx,tsx,ts,json,jsonc}, crate::run_suppression_test, module}其回调run_test(input: static str, _: str, _: str, _: str)接收 4 个参数从输入文件中解析出group/rule路径、读取源码、运行rome_js_analyze::analyze并用insta::assert_snapshot!生成快照。rome_json_analyze在 tests/spec_tests.rs 中tests_macros::gen_tests! {tests/specs/**/*.{json}, crate::run_test, module}rome_js_formatter的 tests/spec_tests.rs 中按语言/模块类型拆分了多组声明js module、js script、ts、jsx、tsx并复用了同一个spec_test::run回调rome_js_formatter的 tests/prettier_tests.rs 还用它跑 Prettier 兼容性用例tests_macros::gen_tests! {tests/specs/prettier/{js,typescript,jsx}/**/*.{js,ts,jsx,tsx}, crate::test_snapshot, script}rome_js_transform在 tests/spec_tests.rs 中用于转换器用例。从这些回调的实现如 rome_js_analyze 的 run_test、rome_js_formatter 的 spec_test::run可以看到第四个参数file_type的真实用法格式化测试中会根据file_type ! module将源码类型切换为ModuleKind::Script。也就是说第三个参数并非摆设而是决定同一批测试如何被解析/格式化的关键开关。六、如何运行与过滤测试原文档给出的运行方式可直接套用且与上文嵌套模块机制配合得很好cargo test # 运行所有 crate 的全部测试 cargo test -p crate-name # 运行某个 crate 的全部测试 cargo test -p crate-name -- some_mod:: # 运行某 crate 中某个模块下的测试 cargo test -p crate-name -- some_mod::somefilename # 只运行某一个测试结合gen_tests!的特性模块名可以是宏展开时自动生成的嵌套目录模块文件名则是cargo test输出中列出的 snake_case 测试名cargo test -p crate-name -- --list可列出全部测试名以便精确定位。七、适用范围与注意事项必须通过 cargo 构建宏依赖CARGO_MANIFEST_DIR环境变量定位基准目录见 get_all_files 中的错误信息脱离 cargo 环境会直接报错glob 与 file_type 只接受字符串字面量编译期解析Arguments::parse文件名为非 UTF-8 时会被跳过并报错AllFiles中File name not UTF8分支expected 配对约定sometest.txt↔sometest.expected.txt文件名含expected的文件不会被当作输入因此可以放心把期望输出文件与输入文件放在同一目录回调函数签名按当前源码实现展开后的测试会以 4 个参数调用回调(test_file: str, test_expected_file: str, test_directory: str, file_type: str)编写回调时请与此保持一致新增用例成本极低在匹配目录中新增输入文件必要时补充.expected文件即可无需改动测试源码——这正是 Rome 能同时维护数千个格式化与 lint 快照用例的根基。综上tests_macros::gen_tests!以一条声明换取了整个测试目录的自动化展开是文件驱动测试模式在 Rust 生态中一个简洁而实用的范例结合 crates/tests_macros/src/lib.rs 的源码与仓库内多个分析器/格式化器的真实用法你可以在自己的 Rust 项目中复刻这套目录即用例、文件即断言的测试基建。【免费下载链接】toolsUnified developer tools for JavaScript, TypeScript, and the web项目地址: https://gitcode.com/gh_mirrors/to/tools创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表