V 语言 main 模块内部测试完全指南:基于 project_with_tests_for_main 演示项目的原理与实战
V 语言 main 模块内部测试完全指南基于 project_with_tests_for_main 演示项目的原理与实战【免费下载链接】vSimple, fast, safe, compiled language for developing maintainable software. Compiles itself in 1s with zero library dependencies. Supports automatic C V translation. https://vlang.io项目地址: https://gitcode.com/GitHub_Trending/v/v导读本篇文章以 V 语言官方测试仓库中的 project_with_tests_for_main 演示项目为骨架系统讲解如何在 V 语言中为main模块内定义的函数编写并运行测试包括内部测试internal tests与外部测试external tests的区别、module main声明的作用、v my_test.v命令背后编译器收集与生成代码的完整机制以及多个_test.v文件共存时的编译策略。读完本文你将能理解 V 测试框架的底层工作原理并直接复用该演示项目模式为你的可执行程序编写可靠的单测。一、背景V 的测试体系与两种测试形态在 V 语言中测试文件以_test.v结尾测试函数名必须以test_开头这是 doc/docs.md 明确规定的两条硬性规则所有测试函数必须位于文件名以_test.v结尾的文件中测试函数名必须以test_开头才会被标记为需要执行的测试。在此基础上V 将测试分为**内部测试internal tests与外部测试external tests**两种形态形态判定方式权限适用场景内部测试测试文件在顶部显式声明module main或与所测模块同名可以直接调用同一模块内的私有函数和私有类型测试本模块内部实现细节外部测试测试文件通过import引入被测模块只能访问模块对外暴露的公共 APIpublic 函数/类型从使用者视角验证模块公共接口project_with_tests_for_main演示项目正是内部测试形态的典型代表它要测试的是main模块内定义的函数因此两个测试文件顶部都以module main开头。注意内部测试必须像同模块的其他.v文件一样声明自己的模块名V 编译器据此判断该测试属于哪个模块。官方文档明确说明sincemodule mainis a regular module like the others, internal tests can be used to test private functions in your main program .v files too因为module main与普通模块无异内部测试同样可以用来测试主程序.v文件中的私有函数。详见 doc/docs.md。二、演示项目解剖一个可复制的 main 模块测试工程project_with_tests_for_main位于 vlib/v/tests/project_with_tests_for_main/目录结构非常精简共 5 个文件project_with_tests_for_main/ ├── README.md # 演示说明本文主题文档 ├── main.v # 被测的主程序定义 iadd() 与 main() ├── my_test.v # 测试文件 1声明 module main ├── my_other_test.v # 测试文件 2声明 module main与文件 1 存在同名测试函数 └── v.mod # 模块元信息1. 被测对象 main.vmain.v 是一个典型的最小主程序fn iadd(x int, y int) int { return x y } fn main() { println(Hello world) println(iadd: iadd(1, 2).str()) }注意main.v顶部没有显式写出module main——V 语言规定编译单个主程序文件时默认模块就是main。iadd是一个普通函数非pub即模块私有main()是程序入口打印两行输出。2. 测试文件 my_test.vmy_test.v 定义了本模块的内部测试module main fn test_iadd_3_4() { a : iadd(3, 4) assert a 7 } fn test_iadd_5_6() { a : iadd(5, 6) assert a 11 }关键点文件第一行声明module main于是编译器知道这是一个main模块的内部测试test_iadd_3_4和test_iadd_5_6可以直接调用main.v中**未导出私有**的iadd函数——这正是内部测试的核心价值。3. 测试文件 my_other_test.v刻意制造函数名冲突my_other_test.v 看起来与my_test.v几乎一样module main fn test_iadd_3_4() { a : iadd(3, 4) assert a 7 assert iadd(10, 20) 30 }它同样声明module main并且故意重复定义了test_iadd_3_4——与my_test.v中的测试函数同名。这是演示项目最有趣的设计它用真实例子验证了 README 中强调的一条规则——每个_test.v文件都被独立编译因此不同测试文件之间即使存在互相冲突的test_函数也完全没有问题Each _test.v file is compiled separately from all othertest.v files, so you can have conflicting testfunctions in them without a problem too。4. 模块元信息 v.modv.mod 声明了该演示项目的模块信息Module { name: project_with_tests_for_main, description: This project demonstrates the ability to test functions in the main module, dependencies: [] }从源码结构看v.mod的存在使该目录成为一个标准 V 模块工程无第三方依赖既可以被v test .这类目录级命令识别也方便作为示例被复制到其他工程中改造使用。三、核心机制一v my_test.v时编译器做了什么README 用一段话概括了内部测试的触发过程当你执行v my_test.v时v 会尝试查找同一文件夹下其他同样在顶部声明了module main的.v文件然后将它们与本测试文件一起处理。这一行为可以在编译器源码中找到精确实现。在 vlib/v/builder/compile.v 中is_test : v.pref.is_test mut is_internal_module_test : false if is_test { tcontent : util.read_file(dir) or { verror(${dir} does not exist) } is_internal_module_test test_file_has_module_declaration(tcontent) } if is_internal_module_test { // v volt/slack_test.v: compile all .v files to get the environment single_test_v_file : os.real_path(dir) if v.pref.is_verbose { v.log( Compiling an internal module _test.v file ${single_test_v_file} .) v.log( That brings in all other ordinary .v files in the same module too .) } user_files single_test_v_file dir os.dir(single_test_v_file) } v.add_file_or_dir(mut user_files, dir)即当以v xxx_test.v方式编译、且该测试文件被判定为内部测试时编译器会把测试文件本身加入编译列表并把目录切换到测试文件所在文件夹随后通过v.add_file_or_dir将该目录下所有普通.v文件一并纳入编译——这正是my_test.v能直接调用main.v中iadd的编译期基础。编译器如何判定内部测试test_file_has_module_declaration判定函数 test_file_has_module_declaration 并非简单地字符串搜索module而是一个小型词法扫描器它逐字符跳过空白、#!shebang 行、//行注释、/* ... */块注释以及[属性]注解然后检查文件第一个有效 token是否为module且后面紧跟空格/制表符。只有满足该条件才返回 true认定这是声明了模块的内部测试文件。这一实现细节解释了 README 中它们测试文件像其他任何内部模块测试一样工作的说法module main必须作为文件的有效首 token 出现注释与属性不影响判定但缺少该声明的_test.v文件会被当作外部测试处理。四、核心机制二fn main(){}为什么不会干扰测试对于内部测试一个自然的疑虑是被测主程序里通常都有fn main(){}测试运行时它会不会也被执行、从而打印一堆无关输出甚至干扰断言README 给出了明确答案你很可能拥有的那个fn main(){}会像平常一样被编译成void main__main(){...}但不会被任何东西调用所以它不会干扰你的测试。该机制同样可以在代码生成器中得到验证。在正常编译路径下vlib/v/gen/c/cmain.v 的gen_c_main会生成main__main();即由生成的 C 语言int main()调用 V 的main__main()。但在测试构建v.pref.is_test时gen_c_main不会生成上述调用——取而代之的是测试专用的int main()见下节于是main__main()虽然被编译为合法符号却处于无人调用状态。相反真正被调用的是一系列test_函数它们会被放入生成的int main(){...}测试运行器test runner中依次执行这与所有_test.v文件无论是内部还是外部测试的行为完全一致。五、核心机制三测试运行器与生成的 int main()1. 测试前奏test runner prelude的注入在测试构建模式下编译器会在编译列表中注入测试运行器前奏文件。见 vlib/v/builder/compile.vC 后端默认注入vlib/v/preludes/test_runner.c.vJS Node 后端注入test_runner.v同时支持通过环境变量VTEST_RUNNER或命令行参数-test-runner指定自定义运行器可选值由pref.supported_test_runners_list()给出自定义路径需以/、\或.v结尾才会被当作文件路径解析否则会被拼接到 preludes 目录下查找test_runner_名字.v。test_runner.c.v中定义了TestRunner接口见 vlib/v/preludes/test_runner.c.v其回调方法覆盖了测试生命周期start(ntests)、finish()、exit_code()、每个测试函数前后的fn_start()/fn_pass()/fn_fail()、每条断言结果的assert_pass()/assert_fail()以及错误传播时的fn_error()。这就是 README 所述测试函数会被放进生成的int main(){...}测试运行器中调用背后的代码实体。2. 生成的 int main() 结构cgen 的测试主函数生成逻辑 展示了测试运行器int main()的生成骨架string v_test_file ...; // 填充文件级元信息并调用 runner.start(测试函数总数) main__VTestFileMetaInfo_free(test_runner.file_test_info); *(test_runner.file_test_info) main__vtest_new_filemetainfo(v_test_file, N); _vtrunner._method_start(_vtobj, N); // 对每一个 test_ 函数 for (...) { // 填充函数级元信息并调用 fn_start *(test_runner.fn_test_info) main__vtest_new_metainfo(name, mod, file, line); _vtrunner._method_fn_start(_vtobj); bool failed false; if (!setjmp(g_jump_buffer)) { test_iadd_3_4(); // 逐个调用 test_ 函数 } else { failed true; // 断言失败通过 longjmp 跳回 } if (failed) { _vtrunner._method_fn_fail(_vtobj); } else { _vtrunner._method_fn_pass(_vtobj); } } _vtrunner._method_finish(_vtobj); return test_exit_code; // 以 runner 计算的退出码返回从中可以清晰读到 README 表述的技术细节生成的int main()会遍历所有test_函数并逐个调用每个函数被包裹在setjmp/longjmp失败捕获机制中before_each/after_each特殊函数也会在此循环中被识别并穿插调用见 cmain.v。断言失败不会立刻终止整个测试程序而是被记录为该测试函数失败继续执行后续测试函数最终由运行器汇总统计并返回退出码。六、多个 _test.v 文件并存独立编译策略README 最后一条 Note 是对多测试文件工程的关键提示每个_test.v文件都会与其他_test.v文件分开独立编译所以你可以在它们里面放心地放置互相冲突的test_函数。这一点在 doc/docs.md 中有更完整的表述所有_test.v文件无论内部还是外部测试都被编译为相互独立的程序正常编译你的业务.v代码时它们完全不影响只有当你显式执行v file_test.v或v test .时它们才会被编译。这正是project_with_tests_for_main中my_test.v与my_other_test.v同时定义test_iadd_3_4仍能顺利通过测试的原因两个文件各自生成一个独立的测试程序函数名冲突只存在于各自的编译单元内部互不干扰。这一设计让开发者可以在不同测试文件里使用相同的辅助测试函数名或将同一场景的测试拆分到多个文件中而无需担心重名。七、运行测试命令与实用选项1. 运行单个测试文件在演示项目目录下执行v my_test.vV 会检测到my_test.v声明了module main→ 判定为内部测试 → 把同目录下其他module main的.v文件这里即main.v一并纳入编译 → 生成独立的测试程序并运行所有test_函数。同理v my_other_test.v2. 运行整个目录 / 模块的测试v test . # 测试当前目录含子目录内的所有测试 v test project_with_tests_for_main # 按模块名测试v test .会扫描当前文件夹及子文件夹中的所有_test.v文件并逐一编译运行。传入-stats选项可以查看每个测试函数更详细的运行统计doc/docs.md。3. 其他相关机制供工程化实践参考套件级钩子可以在测试文件中定义testsuite_begin在所有测试函数之前运行与testsuite_end在所有测试函数之后运行用于统一的准备与清理doc/docs.md。错误返回的测试函数测试函数可以声明错误返回类型如fn test_atoi() !函数内传播出的任何错误都会导致该测试失败doc/docs.md。testdata 目录约定与_test.v同级的testdata文件夹会被测试框架扫描时忽略适合放置故意写错的.v样例或其他测试辅助数据doc/docs.md。通过 VEXE 驱动子测试_test.v内可通过VEXE拿到编译器路径用os.execute运行其他测试文件实现测试内嵌套调用子测试doc/docs.md。八、总结main 模块测试模式的工程价值通过 project_with_tests_for_main 演示项目我们可以提炼出为 V 主程序编写测试的完整模式声明归属测试文件顶部写module main让编译器识别为main模块的内部测试直接调用私有函数内部测试可以无阻碍地访问main.v中未导出的函数与类型覆盖到仅靠公共 API 无法触达的代码路径main()天然隔离被测程序的fn main()会被编译为main__main()但不会被测试运行器调用测试输出保持干净文件间互不干扰每个_test.v独立编译成独立测试程序可放心拆分测试、复用函数名原理有据可查从 compile.v 的收集逻辑、test_file_has_module_declaration 的判定扫描到 test_runner.c.v 的运行器接口与 cmain.v 的int main()生成整条链路在 V 编译器中均有清晰实现可循。对于希望给命令行工具、服务端程序等带main()入口的可执行工程补充单元测试的开发者直接复制该目录结构main.v 若干module main的_test.vv.mod即获得一个最小可用、可逐步扩展的测试骨架。【免费下载链接】vSimple, fast, safe, compiled language for developing maintainable software. Compiles itself in 1s with zero library dependencies. Supports automatic C V translation. https://vlang.io项目地址: https://gitcode.com/GitHub_Trending/v/v创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考