Comprehensive testing patterns for this Neovim plugin. Covers table-driven tests, async patterns, mocking, fixtures, and buffer testing. Apply when writing or reviewing tests.
Writing or reviewing tests in spec/
dofile('spec/minimal_init.lua')require('spec.helpers.buffer_spec') or require('spec.helpers.fixture')after_each with helpers.delete_buffer()vim.api.nvim_set_option_value() or vim.bo[buf]gsub direct return)should [expected behavior]vim.wait() for async operations with vim.scheduledofile('spec/minimal_init.lua')
local helpers = require('spec.helpers.buffer_spec')
describe('module_name', function()
local module = require('github-actions.module_name')
local test_bufnr
before_each(function()
test_bufnr = helpers.create_yaml_buffer('name: Test\non: push')
end)
after_each(function()
helpers.delete_buffer(test_bufnr)
end)
describe('feature', function()
local test_cases = {
{name = 'normal case', input = 'x', expected = 'y'},
{name = 'edge case', input = nil, expected = nil},
}
for _, tc in ipairs(test_cases) do
it(tc.name, function()
local result = module.feature(tc.input)
if tc.expected == nil then
assert.is_nil(result)
else
assert.equals(tc.expected, result)
end
end)
end
end)
end)
local test_cases = {
{name = 'should parse v3', input = 'v3', expected = {3}},
{name = 'should parse v3.5', input = 'v3.5', expected = {3, 5}},
{name = 'should handle nil', input = nil, expected = nil},
}
for _, tc in ipairs(test_cases) do
it(tc.name, function()
assert.are.same(tc.expected, module.parse(tc.input))
end)
end
local helpers = require('spec.helpers.buffer_spec')
before_each(function()
test_bufnr = helpers.create_yaml_buffer([[
name: CI
on: push
jobs:
test:
steps:
- uses: actions/checkout@v3
]])
end)
after_each(function()
helpers.delete_buffer(test_bufnr)
end)
local fixture = require('spec.helpers.fixture')
it('should parse API response', function()
local json_str = fixture.load('gh_api_releases_latest_success')
local result = github.parse_response(json_str)
assert.equals('v3.0.0', result.tag_name)
end)
local stub = require('luassert.stub')
-- Simple return value
stub(github, 'is_available')
github.is_available.returns(false)
-- Complex behavior
stub(vim, 'system')
vim.system.invokes(function(cmd, _, callback)
callback({code = 0, stdout = 'output', stderr = ''})
end)
-- Verify calls
assert.stub(github.is_available).was_called()
assert.stub(github.is_available).was_called(2) -- exactly twice
local called = false
local result
module.async_function(function(value)
called = true
result = value
end)
vim.wait(100, function()
return called
end)
assert.is_true(called)
assert.equals('expected', result)
display.set_version_text(bufnr, version_info)
local ns = display.get_namespace()
local marks = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
assert.equals(1, #marks, 'should have one extmark')
local details = marks[1][4]
local virt_text = details.virt_text
assert.is_true(#virt_text >= 2, 'should have icon and version')
-- Equality
assert.equals(expected, actual)
assert.are.equal(expected, actual)
-- Deep equality (tables)
assert.same({1, 2}, result)
assert.are.same({key = 'val'}, result)
-- Boolean
assert.is_true(value)
assert.is_false(value)
-- Nil checks
assert.is_nil(value)
assert.is_not_nil(value)
-- Type checks
assert.is_string(value)
assert.is_number(value)
-- Pattern matching
assert.matches('pattern', string)
-- Error handling
assert.has.no.errors(function()
module.function(invalid_input)
end)
-- With messages (recommended for complex tests)
assert.equals(1, #marks, 'should have one extmark')
assert.equals(4, mark[2], 'extmark should be on line 4')
describe('cache', function()
before_each(function()
cache.clear()
end)
it('should store value', function()
cache.set('key', 'value')
assert.equals('value', cache.get('key'))
end)
end)
local call_count = 0
stub(git, 'execute_git_command')
git.execute_git_command.invokes(function(_)
call_count = call_count + 1
if call_count == 1 then
return 'first_output', 0, ''
else
return 'second_output', 0, ''
end
end)
it('should pass error to callback when gh unavailable', function()
stub(github, 'is_available')
github.is_available.returns(false)
local err_msg
github.dispatch_workflow('ci.yml', 'main', {}, function(_, err)
err_msg = err
end)
assert.is_not_nil(err_msg)
assert.matches('gh command not found', err_msg)
end)
✅ Use table-driven tests for comprehensive coverage
✅ Clean up resources in after_each
✅ Clear, descriptive test names: should [behavior]
✅ One concept per test
✅ Use helpers for common operations
✅ Document complex data structures in comments
✅ Stub only external dependencies
✅ Include assertion messages for clarity
❌ Test multiple unrelated things in one it block
❌ Leave buffers uncleaned
❌ Use deprecated APIs (nvim_buf_set_option)
❌ Hardcode fixture paths
❌ Create test interdependencies
❌ Mock the code being tested
❌ Return multiple values from type-annotated functions
-- ❌ Bad - returns (string, number)
---@return string
function M.clean(text)
return text:gsub('^["\']', ''):gsub('["\']$', '')
end
-- ✅ Good - returns string only
---@return string
function M.clean(text)
local cleaned = text:gsub('^["\']', ''):gsub('["\']$', '')
return cleaned
end
-- ✅ Good - modern
vim.api.nvim_set_option_value('filetype', 'yaml', {buf = buf})
vim.bo[buf].filetype = 'yaml'
-- ❌ Deprecated
vim.api.nvim_buf_set_option(buf, 'filetype', 'yaml')
describe('module', function()
describe('feature_group_1', function()
it('should handle case A', function() end)
it('should handle case B', function() end)
end)
describe('feature_group_2', function()
it('should handle case C', function() end)
end)
end)
Simple Pure Function:
describe('semver.parse', function()
local test_cases = {
{name = 'v3', input = 'v3', expected = {3}},
{name = 'v3.5.2', input = 'v3.5.2', expected = {3, 5, 2}},
}
for _, tc in ipairs(test_cases) do
it(tc.name, function()
assert.are.same(tc.expected, semver.parse(tc.input))
end)
end
end)
Buffer Operations:
it('should extract workflow name', function()
local buf = helpers.create_yaml_buffer('name: CI\non: push')
local name = detector.get_workflow_name(buf)
assert.equals('CI', name)
helpers.delete_buffer(buf)
end)
With Stubs:
it('should call gh command', function()
local stub = require('luassert.stub')
stub(vim, 'system')
github.fetch_latest_release('owner', 'repo', function() end)
assert.stub(vim.system).was_called()
end)
Remember: Table-driven + helpers + cleanup = maintainable tests
npx skills add skanehira/Lua Testing Best Practices下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer