Lua製Neovim pluginのテストをLuaで書く

2 min read読了の目安(約2600字

NeovimのLua製pluginを実際に動かしてテストするためのコマンドを作った。
CIで動かすためのGitHub Actionも作ったので、なぜ、どんなものを作ったのかをまとめる。

Luaでテストを書く

Vim scriptで書かれたpluginのテストにはthemis.vimが大変便利だけど、
Luaで書かれたpluginのテストはLuaで書きたくなる。
themis.vimから:luaコマンドでの実行も可能そうだが、Neovim本体のリポジトリで使ってるbustedを使うと同じような書き方ができてよさそう。
例えば以下のようにタブを増やすだけのコマンドがあったなら、そのテスト対象のコマンドを実行して結果をassertしたい。

plugin/myplugin.vim
command! MyPluginCommand tabedit
spec/example_spec.lua
describe("MyPluginCommand", function()
  it("can open a tab", function()
    vim.api.nvim_command("MyPluginCommand")

    assert.equals(2, vim.fn.tabpagenr("$"))
  end)
end)

しかし、vimオブジェクトはNeovim内部のLuaのstate初期化時にグローバル(_G)にセットされている。
bustedで直接テストを実行してもvimオブジェクトは存在しない。以下のように怒られる。

$ busted
✱
0 successes / 0 failures / 1 error / 0 pending : 0.000399 seconds

Error → spec/example_spec.lua @ 2
MyPluginCommand can open a tab
spec/example_spec.lua:3: attempt to index global 'vim' (a nil value)

Neovim本体ではビルドされたmoduleをbustedで読み込んで実行しているが、各pluginでビルドの面倒をみるのは大変。
じゃあnvimコマンドを経由してbustedを実行するのが楽そう。

ということで、bustedをnvim --headlessでwrapしたコマンドvustedを作って使っている。

luarocks install vustedでinstall可能。以下のように問題なく実行できる。

$ vusted
ok 1 - MyPluginCommand can open a tab
1..1

テスト上でvimオブジェクトを使える以外はbustedとだいたい同じように使える。
コマンドライン引数もそのまま。--shuffleとかあって便利。
しかし、nvim --headlessで実行してるのでvimのメッセージがテスト結果と一緒に出力されてしまう。
例えば検索時のsearch hit BOTTOM, continuing at TOPや、echoコマンドの出力など。
outputHandlerもTAPに固定していて色付きの格好いい出力はされないが、十分機能はするのであまり問題視していない。
(他のoutputHandlerそのままだとエスケープシーケンスが文字として出ちゃう)

ちなみに、nvim-lua/plenary.nvimにほぼ同じようなmoduleが用意してあって、
そちらではbustedをbundleしている。luaunitも使えるみたい。
vustedを作ったときに存在を知らなかったが、コマンドとして独立していて実行時に何も考えずに使えるものが欲しかったのでヨシ。

CIで動かす

LuaJIT, LuaRocksのセットアップをするためaction-setup-nvim-luaも作った。
(既存のものがWindows未対応だったのと、デフォルトでNeovimに合わせたセットアップを維持するものが欲しかった)
これでGitHub Actionsで楽にテストできるようになった。
Neovim自体のセットアップはaction-setup-vimにお任せしている。とても便利。

jobs:
  test:
    name: Test
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]

    steps:
      - uses: actions/checkout@v2
      - uses: notomo/action-setup-nvim-lua@v1
      - run: luarocks install vusted

      - uses: rhysd/action-setup-vim@v1
        id: vim
        with:
          neovim: true
          version: nightly

      - name: Run tests
        env:
          VUSTED_NVIM: ${{ steps.vim.outputs.executable }}
        run: make test