Testing Neovim plugins is tricky: side effects like spawning jobs (lazydocker), creating windows, notifications, etc.

In lazydocker.nvim tests, we use a mock system to:

  • Simulate missing executables (docker, podman, lazydocker)
  • Capture jobstart params (cmd, env)
  • Stub window APIs (nvim_win_is_valid, nvim_open_win)
  • Intercept vim.notify for error verification

Powered by mini.test child Neovim instances.

Child Neovim: Isolated Sandboxes#

Bootstrap:

local child = helpers.new_child_neovim()  -- MiniTest child
child.setup()  -- Minimal init: readonly=false, small viewport (15x40)
child.load_lzd(config)  -- require('lazydocker').setup(config)
child.unload_lzd()  -- Cleanup: package.loaded, globals, augroups

Lifecycle management:

hooks = {
  pre_case = child.setup,
  post_case = child.unload_lzd,
  post_once = child.stop,
}

Mock Factory System#

tests/mocks.lua exports Mocks table with factories:

local Mocks = {}
Mocks.apply = function(mocks) for _, m in ipairs(mocks) do m.apply() end end
Mocks.restore = function(mocks) ... end  -- Batch apply/restore

Executable Mocks (simulate PATH):

Mocks.vim_fn_executable_no_docker = function(child)
  return create_executable_mock(child, "cmd == 'docker' and 0 or 1")
end
-- Overrides: vim.fn.executable(cmd) → 0 for docker, 1 otherwise

Jobstart Capture:

Mocks.vim_fn_jobstart(child)  -- Logs: _G.mock_logs.jobstart = {cmd='lazydocker', env?, on_exit=fn}
-- Returns fake job ID 99

Notify Spy:

Mocks.vim_fn_notify(child)  -- _G.notify_messages = [{msg=..., level=ERROR}]

Real Test Examples:#

Missing Docker:

local Mocks = { mocks.vim_fn_executable_no_docker(child), mocks.vim_fn_notify(child) }
mocks.apply(Mocks)
lua("LazyDocker.open({ engine = 'docker' })")
eq(#get('_G.notify_messages'), 1)
eq(get('_G.notify_messages')[1].msg, 'LazyDocker: "docker" command not found...')
mocks.restore(Mocks)