ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

codecompanion.nvim 测试指南:基于 mini.test 为 Neovim Lua 插件编写高质量自动化测试

codecompanion.nvim 测试指南:基于 mini.test 为 Neovim Lua 插件编写高质量自动化测试 codecompanion.nvim 测试指南基于 mini.test 为 Neovim Lua 插件编写高质量自动化测试【免费下载链接】codecompanion.nvim✨ AI Coding, Vim Style项目地址: https://gitcode.com/GitHub_Trending/co/codecompanion.nvim导读本文以 codecompanion.nvim 仓库内的实践文档.codecompanion/tests/mini_test_testing.md为骨架系统讲解如何使用mini.test为 Neovim Lua 插件编写测试从测试文件组织、内置断言、hooks、参数化测试到基于子进程的屏幕截图断言与自定义测试运行并结合 codecompanion.nvim 自身的测试代码tests/目录给出可直接落地的实战模板。读完本文你将能够为自己的 Neovim 插件搭建一套既支持交互式调试、又能在 CI 中无头运行的测试体系。为什么选择 mini.test为 Neovim Lua 插件写测试很难写好更难。mini.test的设计目标是在保持足够灵活性的同时把难度降到可接受的水平它刻意选择了一种更啰嗦、更程序化的测试书写风格与基于plenary.nvim的 busted 风格describe/it形成对照。前者更贴近直接写函数、直接断言的 Lua 直觉后者则偏重 DSL 化的可读性。需要强调的是mini.test 默认也支持 busted 风格两种写法可以并存。本文其余部分统一采用官方推荐的 test set测试集方式。整体组织方式一个测试文件对应一个测试集文档给出的通用方法论非常清晰按 Lua 文件组织测试每个测试文件都位于tests/目录下并以test_前缀命名如tests/test_hello_lines.lua每个文件返回一个测试集测试集由MiniTest.new_set()创建每个测试动作是绑定在测试集字段上的独立函数函数抛错即测试失败强烈建议在测试动作内使用自定义的子 Neovim 进程即 child process执行真实测试避免污染当前会话。完整推荐的项目结构如下其中Mandatory为必须Recommended为推荐. ├── deps │ └── mini.nvim # 必须mini.test 依赖 ├── lua │ └── hello_lines │ └── init.lua # 必须被测插件 ├── Makefile # 推荐 ├── scripts │ ├── minimal_init.lua # 必须无头 Neovim 启动脚本 │ └── minitest.lua # 推荐 └── tests └── test_hello_lines.lua # 必须测试文件这一结构在 codecompanion.nvim 中得到了完整印证仓库根目录有Makefile内含test、test_file目标scripts/minimal_init.lua负责初始化最小环境tests/下按模块划分了大量test_*.lua文件。搭建最小测试环境1. 引入 mini.nvim 依赖建议将 mini.nvim 存放在deps/mini.nvim目录用git拉取--filterblob:none为部分克隆可显著减少下载量mkdir -p deps git clone --filterblob:none mini.nvim 仓库地址 deps/mini.nvim2. 最小启动脚本 scripts/minimal_init.lua该脚本保证 Neovim 进程能识别被测插件与 mini.test 依赖仅在无头模式#vim.api.nvim_list_uis() 0下才初始化 mini.test避免影响交互式使用-- 把当前目录加入 runtimepath以便使用 lua 文件 vim.cmd([[let rtp.,.getcwd()]]) -- 仅在调用无头 Neovim 时如 make test设置 mini.test if #vim.api.nvim_list_uis() 0 then -- 假设 mini.nvim 存放在 deps/mini.nvim vim.cmd(set rtpdeps/mini.nvim) -- 设置 mini.test require(mini.test).setup() end对照 codecompanion.nvim 的scripts/minimal_init.lua可以看到它在此基础上进一步强化了测试可控性引入deps/plenary.nvim、deps/nvim-treesitter、固定颜色主题保证截图稳定并把所有可能触网的 HTTP 方法替换为直接报错的函数——任何测试若尝试真实网络请求都会立刻失败。这是一种值得借鉴的测试边界策略测试环境内禁止一切不可控的外部副作用。3. Makefile 简化运行与 CI 集成文档推荐用 Makefile 同时服务本地命令行与 CI模板如下# 运行全部测试文件 test: deps/mini.nvim nvim --headless --noplugin -u ./scripts/minimal_init.lua -c lua MiniTest.run() # 运行 $FILE 环境变量指定的测试文件 test_file: deps/mini.nvim nvim --headless --noplugin -u ./scripts/minimal_init.lua -c lua MiniTest.run_file($(FILE)) # 下载 mini.nvim deps/mini.nvim: mkdir -p deps git clone --filterblob:none mini.nvim 仓库地址 $codecompanion.nvim 的Makefile与之一致地实现了test目标并额外设置了LC_ALLC以保证输出环境稳定test: deps echo Testing... LC_ALLC nvim --headless --noplugin -u ./scripts/minimal_init.lua -c lua MiniTest.run()4. 两种运行方式mini.test 开箱即用地支持两种运行模式交互式在当前 Neovim 会话内直接执行:lua MiniTest.run()/:lua MiniTest.run_file()/:lua MiniTest.run_at_location()。默认配置下结果展示在一个浮动窗口中按q关闭。这种方式非常适合调试测试本身但可能影响当前环境因此测试内部应使用子进程来规避副作用。无头模式从 shell用启动脚本拉起无头 Neovim 并执行lua MiniTest.run()配合上述 Makefile 即为make test结果直接打印在 shell 中是 CI 的标准姿势。第一个测试从函数到断言测试集字段上的函数就是测试用例函数抛错则测试失败测试文件返回单个测试集。最朴素的写法local T MiniTest.new_set() T[works] function() local x 1 1 if x ~ 2 then error(x is not equal to 2) end end return T手写if ... error() ... end太过繁琐因此 mini.test 内置了最小但够用的断言集合MiniTest.expectlocal T MiniTest.new_set() T[works] function() local x 1 1 MiniTest.expect.equality(x, 2) end return T测试集支持嵌套这为后面结合 hooks 与参数化打下了基础local T MiniTest.new_set() T[big scope] new_set() T[big scope][works] function() MiniTest.expect.equality(1 1, 2) end T[big scope][also works] function() MiniTest.expect.equality(2 2, 4) end T[out of scope] function() MiniTest.expect.equality(3 3, 6) end return T若偏爱 busted 风格也可等价写成describe/it此时不要返回测试集describe(big scope, function() it(works, function() MiniTest.expect.equality(1 1, 2) end) end) it(out of scope, function() MiniTest.expect.equality(3 3, 6) end)内置断言与自定义断言最常用的四个内置断言如下eq是MiniTest.expect.equality的常用别名因其高频使用而值得单独定义local T MiniTest.new_set() local expect, eq MiniTest.expect, MiniTest.expect.equality local x 1 1 T[expect.equality] function() eq(x, 2) end T[expect.no_equality] function() expect.no_equality(x, 1) end T[expect.error] function() -- 函数抛错则通过 expect.error(function() if x 2 then error(Deliberate error) end end) end T[expect.no_error] function() -- 函数不抛错则通过 expect.no_error(function() if x ~ 2 then error(This should not be thrown) end end) end return T对于反复出现的断言模式MiniTest.new_expectation(subject, predicate, fail_context)允许封装自定义断言三个参数分别是断言主题描述、谓词函数、失败上下文格式化函数。例如封装字符串匹配断言local T MiniTest.new_set() local expect_match MiniTest.new_expectation( string matching, -- 断言主题 function(str, pattern) return str:find(pattern) ~ nil end, -- 谓词 function(str, pattern) -- 失败上下文 return string.format(Pattern: %s\nObserved string: %s, vim.inspect(pattern), str) end ) T[string matching] function() local x abcd expect_match(x, ^a) -- 通过 expect_match(x, x) -- 失败 end return T该用例失败时输出来自文档原文FAIL in tests/test_basics.lua | string matching: Failed expectation for string matching. Pattern: x Observed string: abcd Traceback: tests/test_basics.lua:20codecompanion.nvim 在tests/expectations.lua中大量使用该机制定义了一批带良好失败信息的自定义断言例如is_true、is_false、eq_info在eq基础上增加第三条说明消息并通过tests/helpers.lua合并进公共 helper 供所有测试复用H.eq_info MiniTest.new_expectation( values are equal, function(left, right) return vim.deep_equal(left, right) end, function(left, right, msg) return string.format(Left: %s\nRight: %s\nMsg: %s, vim.inspect(left), vim.inspect(right), msg) end )Hooks在固定阶段注入行为hooks 是无参函数在测试执行的规定阶段被调用属于测试集级别的特性共四种Hook触发时机pre_once第一个过滤后的节点执行前pre_case每个用例执行前含嵌套post_case每个用例执行后含嵌套post_once最后一个过滤后的节点执行后示例体会计数递增的顺序逻辑local new_set MiniTest.new_set local expect, eq MiniTest.expect, MiniTest.expect.equality local T new_set() local n 0 local increase_n function() n n 1 end T[hooks] new_set({ hooks { pre_once increase_n, pre_case increase_n, post_case increase_n, post_once increase_n }, }) T[hooks][work] function() -- 此时 n 被递增两次pre_once 与 pre_case eq(n, 2) end T[hooks][work again] function() -- 再递增两次上一用例的 post_case 与本用例的 pre_case eq(n, 4) end T[after hooks set] function() -- 再递增两次上一用例的 post_case 与 T[hooks] 的 post_once eq(n, 6) end return Thooks 最常见的实战用途是管理子进程生命周期——在pre_case中重启干净的子进程并加载被测插件在post_once中停止子进程详见后文子进程测试一节。codecompanion.nvim 的测试大量遵循此模式例如tests/test_init.lua中T[cli()]测试集在pre_once里启动子进程并注入测试配置pre_case里清理上一次用例遗留的 buffer 与窗口post_once统一child.stop。参数化测试一份用例、多组输入参数化是 mini.test 的显著特性与 hooks 一样属于测试集特性。每组参数都是一个数组以便一次传入多个实参多个参数化层级会做笛卡尔积相乘。local new_set MiniTest.new_set local eq MiniTest.expect.equality local T new_set() -- 每组参数是数组允许同时参数化多个实参 T[parametrize] new_set({ parametrize { { 1 }, { 2 } } }) -- 展开为两个用例第一个失败 T[parametrize][works] function(x) eq(x, 2) end -- 嵌套参数化用例数相乘2 × 2 4 个其中两个失败 T[parametrize][nested] new_set({ parametrize { { 1 }, { 2 } } }) T[parametrize][nested][works] function(x, y) eq(tostring(x), y) end -- 多实参参数化两组都通过 T[parametrize multiple arguments] new_set({ parametrize { { 1, 1 }, { 2, 2 } } }) T[parametrize multiple arguments][works] function(x, y) eq(x, y) end return T在文档的hello_lines示例中set_lines()用parametrize { {}, { 0, { a } }, { 0, { 1, 2, 3 } } }一次性覆盖了默认参数、单行输入与多行输入三种场景。n_retry缓解固有偶发失败某些测试天生不稳定如验证带延迟的事件序列在慢速 CI 机器上可能偶发失败。测试集的n_retry字段表示每个用例最多重试多少次直至成功local new_set MiniTest.new_set local T new_set() -- 每个用例最多尝试 5 次 T[n_retry] new_set({ n_retry 5 }) -- n_retry 1 时每两次运行失败 1 次n_retry 5 时约每 32 次失败 1 次 T[n_retry][case] function() math.randomseed(vim.loop.hrtime()) assert(math.random() 0.5) end return T在 codecompanion.nvim 的测试里也有直接体现如tests/interactions/chat/tools/builtin/test_run_command.lua中通过vim.wait(...)轮询异步状态来等待工具执行完成这类依赖时序的用例正是n_retry的典型适用对象。MiniTest.current 与用例辅助函数MiniTest.current表保存当前执行的测试信息all_cases是所有将被执行的用例顺序化的数组case是当前正在执行的用例。每个 case 是一个顺序执行的最小单元包含args、data、desc、exec执行状态、失败列表、备注、hooks、test等字段。文档还给出一个实用调试技巧故意断言eq(MiniTest.current.all_cases, 0)制造失败从而在失败信息中打印出完整的 case 结构含各字段默认值是理解内部数据结构的最快途径。与之配套的用例辅助函数MiniTest.skip(reason)跳过剩余执行并留下说明性备注用例以with notes状态通过MiniTest.add_note(msg)为用例添加备注最终状态带 with notes 后缀MiniTest.finally(fn)注册一个函数无论本用例成功还是出错都会在该用例结束后执行。综合示例与输出来自文档原文local T MiniTest.new_set() T[skip()] function() if 1 1 2 then MiniTest.skip(Apparently, 1 1 is 2) end error(1 1 is not 2) end T[add_note()] function() MiniTest.add_note(This test is not important.) error(Custom error.) end T[finally()] function() MiniTest.finally(function() if #MiniTest.current.case.exec.fails 0 then MiniTest.add_note(This test is flaky.) end end) error(Expected error from time to time) end return TNOTE in tests/test_basics.lua | skip(): Apparently, 1 1 is 2 FAIL in tests/test_basics.lua | add_note(): tests/test_basics.lua:16: Custom error. NOTE in tests/test_basics.lua | add_note(): This test is not important. FAIL in tests/test_basics.lua | finally(): tests/test_basics.lua:28: Expected error from time to time NOTE in tests/test_basics.lua | finally(): This test is flaky.其中finally()的实现模式——检测#MiniTest.current.case.exec.fails 0后追加此用例易碎的备注——是标记 flaky 测试的实用技巧。自定义测试运行收集与执行两个阶段一次测试运行分为两个阶段配置全部通过MiniTest.run({ ... })的opts参数传入收集Collectionsource 每个合适的文件可自定义把所有测试集合并为单个测试集再转换为顺序化的用例数组最后按可自定义的谓词过滤用例执行Execution以调度式异步的方式逐个安全执行用例依次经历 pre-hooks、测试动作、post-hooks并把结果写入MiniTest.current.all_cases中对应 case 的exec字段执行本身不产生输出同时调用可自定义的 reporter 方法来实时展示结果。收集自定义文件与过滤利用测试集的data字段携带自定义信息如快慢分类再通过collect.find_files与collect.filter_cases定制收集范围local new_set MiniTest.new_set local T new_set() T[fast] new_set({ data { type fast } }) T[fast][first test] function() end T[fast][second test] function() end T[slow] new_set({ data { type slow } }) T[slow][first test] function() vim.loop.sleep(1000) end T[slow][second test] function() vim.loop.sleep(1000) end return T只跑单个文件中的 fast 用例MiniTest.run({ collect { find_files function() return { tests/test_basics.lua } end, filter_cases function(case) return case.data.type fast end, } })执行自定义 reporter 与失败即停reporter 需要实现start(cases)、update(case_num)、finish()等方法。例如结束后在命令行打印状态汇总表local reporter { finish function() local summary {} for _, c in ipairs(MiniTest.current.all_cases) do local state c.exec.state summary[state] (summary[state] or 0) 1 end print(vim.inspect(summary, { newline , indent })) end, } MiniTest.run({ execute { reporter reporter } })execute还支持stop_on_first_error等选项用于首个用例失败即中止整轮执行节省调试时间。子进程测试mini.test 的灵魂功能mini.test 与其他 Lua 测试框架最大的不同在于在测试中刻意使用自定义的子 Neovim 进程理想情况下每个测试都应在一个仅加载了最小配置如能加载被测插件的全新 Neovim 进程中完成。MiniTest.new_child_neovim()返回一个子进程对象附带大量实用方法启动/停止/重启、重定向执行在当前进程写代码、在子进程执行、模拟按键、测试屏幕状态等。原理与限制当前进程与子进程通过 RPC 消息通信见:h RPC因此可以在子进程内编程执行代码、取回输出并断言。子进程是无头但功能完整的 Neovim足以测试 extmarks、浮动窗口等界面特性。文档同时如实指出了两点局限受 RPC 协议实现限制函数与 userdata 无法在子进程的输入输出中传递报错特征为Cannot convert given lua type通常解法是把逻辑搬到子进程一侧如定义并使用全局函数注意重启后会被遗忘偶尔出现挂起进程停止执行且无输出多因 Neovim 被阻塞等待用户输入常见诱因是 hit-enter 提示调高提示高度解决或 Operator-pending 模式退出该模式解决。大多数辅助方法在能推断立即执行会导致挂起时会主动抛错。推荐的生命周期管理在每个用例前重启干净子进程全部用例结束后统一停止local child MiniTest.new_child_neovim() local T MiniTest.new_set({ hooks { pre_case function() -- 用自定义 init.lua 重启子进程 child.restart({ -u, scripts/minimal_init.lua }) -- 加载被测插件 child.lua([[M require(hello_lines)]]) end, -- 全部用例结束后停止 post_once child.stop, }, }) -- 在此定义测试 return T执行 Lua 代码lua() 与 lua_get()child.lua(code, ...)在子进程中执行任意 Lua 字符串是vim.api.nvim_exec_lua()的封装child.lua_get(s, ...)等价于child.lua(return .. s, ...)适合取返回值local eq MiniTest.expect.equality local child MiniTest.new_child_neovim() local T MiniTest.new_set({ hooks { pre_case function() child.restart({ -u, scripts/minimal_init.lua }) child.lua([[M require(hello_lines)]]) end, post_once child.stop, }, }) T[lua()][works] function() child.lua(_G.n 0; _G.n _G.n 1) eq(child.lua(return _G.n), 1) end T[lua()][can use tested plugin] function() eq(child.lua(return M.compute()), { Hello world }) eq(child.lua([[return M.compute({a, b})]]), { Hello a, Hello b }) end T[lua_get()] function() child.lua(_G.n 0) eq(child.lua_get(_G.n), child.lua(return _G.n)) end return Tcodecompanion.nvim 对其进行了再封装tests/helpers.lua里的Helpers.child_start(child)在重启后还会清空 statusline、laststatus与cmdheight保证子进程界面状态一致是最小化不可控因素的典型处理。管理选项与状态重定向表仅靠字符串式 Lua 执行写复杂测试会迅速变得笨拙因此 mini.test 提供了大量重定向便捷助手——在当前进程写代码自动在子进程执行child.api.xxx(...)等价于在子进程执行vim.api.xxx(...)并返回结果经vim.rpcrequest()child.cmd(...)/child.cmd_capture(...)执行 Vimscript后者捕获输出child.fn.*、child.loop.*、child.lsp.*等覆盖大部分 Neovim 核心功能的vim.fn/vim.loop/vim.lsp重定向表作用域变量与选项重定向表child.b.*buffer 变量、child.w.*、child.t.*、child.v.*、child.g.*以及child.bo.*/child.wo.*/child.o.*选项表均可读写。示例local new_set MiniTest.new_set local eq MiniTest.expect.equality local child MiniTest.new_child_neovim() local T MiniTest.new_set({ hooks { pre_case function() child.restart({ -u, scripts/minimal_init.lua }) child.lua([[M require(hello_lines)]]) end, post_once child.stop, }, }) T[api()/api_notify()] function() -- 首个 buffer 默认 readonly会拖慢测试先关掉 child.api.nvim_set_option_value(readonly, false, { buf 0 }) child.api.nvim_buf_set_lines(0, 0, -1, true, { aaa }) eq(child.api.nvim_buf_get_lines(0, 0, -1, true), { aaa }) end T[cmd()/cmd()] function() child.cmd(hi Comment guifg#aaaaaa) eq(child.cmd_capture(hi Comment), Comment xxx guifg#aaaaaa) end T[various redirection tables with methods] function() eq(child.fn.fnamemodify(hello_lines.lua, :t:r), hello_lines) eq(child.loop.hrtime() 0, true) eq(child.lsp.get_clients(), {}) end T[redirection tables for variables] function() child.b.aaa true eq(child.b.aaa, true) eq(child.b.aaa, child.lua_get(vim.b.aaa)) end T[redirection tables for options] function() child.o.lines, child.o.columns 5, 12 eq(child.o.lines, 5) eq({ child.o.lines, child.o.columns }, child.lua_get({ vim.o.lines, vim.o.columns })) end return T模拟用户按键type_keys()child.type_keys()用于模拟用户输入支持单字符串、多字符串与可嵌套的字符串表还可指定自定义延迟local eq MiniTest.expect.equality local child MiniTest.new_child_neovim() local T MiniTest.new_set({ hooks { pre_case function() child.restart({ -u, scripts/minimal_init.lua }) child.bo.readonly false child.lua([[M require(hello_lines)]]) end, post_once child.stop, }, }) local get_lines function() return child.api.nvim_buf_get_lines(0, 0, -1, true) end T[type_keys()][works] function() -- 单个字符串 child.type_keys(iabcdeEsc) eq(get_lines(), { abcde }) eq(child.fn.mode(), n) -- 多个字符串提升可读性 child.type_keys(cc, fghij, Esc) eq(get_lines(), { fghij }) -- 字符串表可嵌套 child.type_keys({ cc, { j, k, l, m, n } }) eq(get_lines(), { jklmn }) end T[type_keys()][allows custom delay] function() -- 每个字符串之后附加 500ms 延迟 child.type_keys(500, i, abcde, Esc) eq(get_lines(), { abcde }) end return T屏幕截图断言验证长什么样验证插件是否以预期方式显示高亮、statusline、tabline、sign column、extmark 等是 Neovim 插件测试的一大难点。mini.test 的做法是child.get_screenshot()对每个可见单元格调用screenstring()与screenattr():h screenstring()/:h screenattr()返回包含两层的截图表text 层按行列组织的二维数组记录每个单元格显示的字符attr 层按行列组织的二维数组记录单元格外观高亮的编码符号只在相对意义上有效——两个单元格符号相同/不同意味着视觉外观相同/不同。两个重要注意点由于依赖screenattr()截图无法告知某单元格具体是什么高亮只能判断两个单元格是否高亮一致Neovim 本身的限制未来可能改善当不同属性值超过 94 种时会出现误判因为输出被编码为单字符符号。配合MiniTest.expect.reference_screenshot(screenshot, path, opts)使用首次运行时自动在指定路径创建参考截图路径缺省时由用例描述推断默认落在tests/screenshots/目录后续运行将其与当前截图比对不一致则抛出带详细提示的错误。示例结合参数化一次生成多份参考截图local expect MiniTest.expect local child MiniTest.new_child_neovim() local T MiniTest.new_set({ hooks { pre_case function() child.restart({ -u, scripts/minimal_init.lua }) child.bo.readonly false child.lua([[M require(hello_lines)]]) end, post_once child.stop, }, }) T[set_lines()] MiniTest.new_set({ parametrize { {}, { 0, { a } }, { 0, { 1, 2, 3 } } } }) T[set_lines()][works] function(buf_id, lines) child.o.lines, child.o.columns 10, 15 child.lua(M.set_lines(...), { buf_id, lines }) expect.reference_screenshot(child.get_screenshot()) end return T这会生成三份截图文件文件名含用例描述与参数例如参数为{ 0, { 1, 2, 3 } }的参考截图内容如下带行号与列标尺便于比对--|---------|----- 01|Hello 1 02|Hello 2 03|Hello 3 04|~ 05|~ 06|~ 07|~ 08|~ 09|e] [] 1,1 All 10| --|---------|----- 01|000001111111111 02|000001111111111 03|000001111111111 04|222222222222222 05|222222222222222 06|222222222222222 07|222222222222222 08|222222222222222 09|333333333333333 10|444444444444444文本层清晰展示Hello 1/2/3、空行~与状态行attr 层则用符号编码表示不同外观区域如Hello前缀的Special高亮、普通文本与状态行各不相同。需要更新参考截图时可删除对应截图文件后重跑用例或临时给reference_screenshot()加{ force true }强制覆盖。这正是 codecompanion.nvim 测试体系大量采用的断言方式仓库的tests/screenshots/目录中保存着数十份参考截图例如tests-interactions-chat-tools-builtin-test_run_command.lua---run_command-tool、tests-test_init.lua---cli()---prompttrue-opens-the-input-buffer等测试文件里则通过h.expect_screenshot(child.get_screenshot())见tests/expectations.lua与tests/interactions/chat/tools/builtin/test_run_command.lua完成屏幕状态级别的回归验证。通用技巧与实战建议1. 建立共享 helpers 文件创建tests/helpers.lua集中存放多文件复用的代码可对 mini.test 函数做monkey-patch式封装。例如统一子进程的启动与插件加载local Helpers {} Helpers.new_child_neovim function() local child MiniTest.new_child_neovim() child.setup function() child.restart({-u, scripts/minimal_init.lua}) child.bo.readonly false child.lua([[M require(hello_lines)]]) end return child end return Helperscodecompanion.nvim 的tests/helpers.lua是这一实践的完整范例它把测试配置注入setup_plugin、mock HTTP 客户端mock_http/queue_mock_http_response/get_mock_http_requests、mock 聊天提交mock_submit/restore_submit、搭建聊天 buffersetup_chat_buffer/teardown_chat_buffer、模拟工具调用make_tool_call等高频操作全部封装为函数并复用tests/expectations.lua中的自定义断言通过vim.tbl_extend(error, Helpers, require(tests.expectations))合并。值得注意的是其mock_http_client()的实现思路让所有 HTTP 请求默认永不回调、setup_chat_buffer注入tests.config中定义的本地test_adapter见tests/config.lua——测试真正聚焦于插件自身的逻辑链路。2. 在文件顶部为常用函数写别名提升可读性并减少重复-- 某段初始化 child 的代码 local set_lines function(lines) child.api.nvim_buf_set_lines(0, 0, -1, true, lines) end3. 注意自动命名截图的平台限制使用自动命名的截图时注意部分系统如 Windows、macOS文件系统大小写不敏感两个仅大小写不同的文件名会引发问题部分系统对完整路径长度如部分 GitWindows 组合的 260 字节或文件名长度有限制如 ext4 Windows 分区的 255 字节、eCryptfs Linux 分区的 143 字节。建议通过缩短用例名把文件名控制在 143 字节以内路径长度则难以完全规避尽量让目录层级扁平一些。4. 用 tree-sitter 高亮内嵌 Lua 代码为便于阅读child.lua/child.lua_get中作为字符串的 Lua 代码可在个人配置的after/queries/lua/injections.scm中添加如下 capture文件开头必须有; extends见:h treesitter-query-modeline-extends; extends (function_call name: (dot_index_expression table: (identifier) _table field: (identifier) _field) arguments: (arguments (string content: (string_content) injection.content)) (#eq? _table child) (#any-of? _field lua lua_get) (#set! injection.language lua))分层测试思路单元断言 子进程集成 真实链路验证把 mini.test 的各个能力组合起来可以搭建一套分层测试策略这也正是 codecompanion.nvim 仓库的实际形态纯逻辑断言不依赖 Neovim 状态的函数如消息组装、参数转换直接在当前进程用eq/ 自定义断言验证子进程集成测试涉及 buffer、窗口、extmark、输入模拟的功能在pre_case重启子进程、加载插件后配合child.api.*、child.type_keys()与expect.reference_screenshot做端到端行为验证参见tests/test_init.lua、tests/interactions/chat/tools/builtin/test_run_command.lua等大量用例真实模型链路测试可选仓库还提供了tests/scripts/tool_testing/README.md描述的一套独立于 mini.test 的工具回归测试方案——用真实 LLM adapter 驱动 chat buffer、等待工具执行后校验结果以捕捉单元测试无法覆盖的模型输出接进完整工具编排器才出现的回归并支持--csv跨轮次累计对比、--repeat测量一致性等运维能力。在编写测试时始终牢记 mini.test 的核心设计哲学每个用例都应在一个全新、最小化的 Neovim 进程中运行。hooks 负责环境初始化与清理参数化负责场景覆盖截图断言负责视觉回归而n_retry与finally()则帮你驯服偶发的时序问题——这套组合拳足以覆盖 Neovim Lua 插件测试的绝大多数场景。【免费下载链接】codecompanion.nvim✨ AI Coding, Vim Style项目地址: https://gitcode.com/GitHub_Trending/co/codecompanion.nvim创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表