🐶

Luaのテストツール busted の使い方

2022/09/14に公開

bustedとは(vustedとは)

Neovim の Lua プラグインのテストを行えるvustedというツールがあることを最近知りました。

vustedbustedという Lua のテストツールを Neovim でも動くようラップしたもので、vusted を使用するには busted の使い方を調べる必要があります。

https://github.com/lunarmodules/busted

本記事ではこの busted の使い方を簡単に説明していきます。busted を使ってみたいけど日本語記事がないからよく分からん。という人向けの記事ですね。

より詳しく確認したい人はこちらbusted 公式ドキュメントを確認してください。

Usage

簡単な使い方を説明します。

-- init_spec.lua
describe("Test", function()
  describe("numerical", function()
    it("'0' is truthy", function()
      assert.is_truthy(0)
    end)

    -- failed test
    it("'1' equal '0'", function()
      assert.is_equal(1, 0)
    end)
  end)

  describe("non-numerical", function()
    it("nil is falsy", function()
      assert.is_falsy(nil)
    end)

    it("table value is same", function()
      assert.is_same({ value = 'same' }, { value = 'same' })
    end)

    it("object is equal", function()
      local a = { obj = 'same' }
      local b = a
      assert.is_equal(a, b)
    end)
  end)
end)

assert.XXXX でテストを行い、itdescribe にはどんなテストを行うのか記載します。describe はネストして使えるので、テスト対象をグループ分けできます。

基本的に it には 1 つの assert のみを記載してください。複数の assert を記載した場合、テストが失敗した際にどの assert で失敗したかが出力結果から分かりづらくなってしまいます。
ちなみに describe には context というエイリアスが存在します。同じく it にも spec というエイリアスが存在します。
ではこのテストファイルを busted で実行してみましょう。

rapan931@rHost:~/tmp/lua$ busted init_spec.lua  --output=TAP
ok 1 - Test numerical '0' is truthy
not ok 2 - Test numerical '1' equal '0'
# init_spec.lua @ 66
# Failure message: init_spec.lua:67: Expected objects to be equal.
# Passed in:
# (number) 0
# Expected:
# (number) 1
ok 3 - Test non-numerical nil is falsy
ok 4 - Test non-numerical table value is same
ok 5 - Test non-numerical object is equal
1..5

1 と 0 がイコールな訳はなくこちらのサンプルは極端すぎますが、describe,it に何をテストするのか記載しておくと、出力されたメッセージを見て何が成功して何が失敗したか分かりやすくなります。

コマンド実行時に --output=TAP を指定して出力フォーマットを変更していますが、TAP 以外にも utfTerminal(これがdefault)json 等用意されています。出力フォーマットはカスタム可能なので、好みの出力が用意されていない場合は独自の出力に変更できます。

before_each, after_each, setup, teardown

describe,it ブロック内で before_each,after_each を使用し対象ブロック内のテスト前後に実行する処理を設定できます。また、setup,teardown を呼び出すと対象ブロックの最初と最後に実行する処理を設定できます。

describe("describe 1", function()
  before_each(function() print("before 1") end)
  after_each(function() print("after 1") end)
  setup(function() print("setup 1") end)
  teardown(function() print("teardown 1") end)

  describe("- 1", function()
    it("it 1", function() assert.is_equal(1, 1) end)
  end)

  describe("- 2", function()
    before_each(function() print("before 2") end)
    after_each(function() print("after 2") end)
    setup(function() print("setup 2") end)
    teardown(function() print("teardown 2") end)

    it("it 2", function() assert.is_equal(1, 1) end)
    it("it 3", function() assert.is_equal(1, 1) end)
  end)

  describe("- 3", function()
    it("it 4", function() assert.is_equal(1, 1) end)
  end)
end)

実行結果がこちら

rapan931@rHost:~/tmp/lua$ busted init_spec.lua --output=TAP
setup 1
before 1
ok 1 - describe 1 - 1 it 1
after 1
setup 2
before 1
before 2
ok 2 - describe 1 - 2 it 2
after 2
after 1
before 1
before 2
ok 3 - describe 1 - 2 it 3
after 2
after 1
teardown 2
before 1
ok 4 - describe 1 - 3 it 4
after 1
teardown 1
1..4

tag

describe,it にタグをつけて実行するテストを絞りこむことができます

describe("a test #tag", function()
  -- tests go here
end)

describe("a nested block #another", function()
  describe("can have many describes", function()
    -- tests
  end)
end)

busted init_spec.lua -t tag#tag が付いたブロックのみをテストできます。
逆に busted init_spec.lua --exclude-tag tag#tag が付いたブロックのテストを除外できます。

shuffle

busted --shuffle でテストの実行順序をランダムにできます。一部テストのみ記載順通りに実行したい場合は対象ブロック内に randomize(false) を記載してください

assert

assert ですが、色々あります

describe("assert list", function()
  it("test", function()
    assert.True(true) -- trueはluaの予約語なので先頭大文字
    assert.is_true(true)
    assert.is_truthy(0)

    assert.False(false)
    assert.is_false(false)
    assert.is_falsy(nil)

    assert.are.equal(1, 1)
    assert.is.equal(1, 1) -- areはisのエイリアス。is_equal, are_equalも同じ結果になる

    assert.is_not_true(false)
    assert.are_not_equals(1, "1") -- 否定にはnotを付ける

    local a = { key = "value" }
    local b = a
    assert.is_equal(a, b) -- 同じインスタンスかチェック

    assert.is_same("hogehoge", "hogehoge") -- 同じ値かチェック
    assert.is_same({ name = "hoge" }, { name = "hoge" }) -- tableの場合は再帰的に同じ値かチェックする
  end)
end)

独自のチェック関数の追加もできます。
例として文字列の先頭 1 文字目が英字であることを確認する assert を追加してみましょう

local say = require("say")
local function start_witch_alphabet(_, args)
  if not type(args[1]) == "string" or #args ~= 1 then
    return false
  end
  if args[1]:match([=[^[a-zA-Z]]=]) then
    return true
  end
  return false
end

say:set("assertion.start_witch_alphabet.positive", "Expected string begin with an alphabetic character: %s")
say:set("assertion.start_witch_alphabet.negative", "Expected string not begin with an alphabetic character: %s")
assert:register("assertion", "start_witch_alphabet", start_witch_alphabet, "assertion.start_witch_alphabet.positive", "assertion.start_witch_alphabet.negative")

describe("test", function()
  it("begin alphabetic character", function()
    assert.start_witch_alphabet('h123')
  end)
  it("not begin alphabetic character", function()
    assert.not_start_witch_alphabet('0123')
  end)
end)

サクッと独自の assert を追加できます。

ちなみに、busted には language pack が存在し、これを使用すれば busted 実行時の出力メッセージを日本語に変更できます。
日本語用ファイルの中身見たのですが、面白いメッセージがいくつか格納されていました。
以下に変更後 busted --lang=ja init_spec.lua でテストを実行してみてください。シャレオツなエラーメッセージを見ることができますので、気になる人は試してみてください。

say:set("assertion.start_witch_alphabet.positive", require('busted.languages.ja').failure_messages[6] .. ": %s") -- busted.language.jaのメッセージを使用
say:set("assertion.start_witch_alphabet.negative", require('busted.languages.ja').failure_messages[7] .. ": %s") -- busted.language.jaのメッセージを使用
assert:register("assertion", "start_witch_alphabet", start_witch_alphabet, "assertion.start_witch_alphabet.positive", "assertion.start_witch_alphabet.negative")

describe("test", function()
  it("begin alphabetic character", function()
    assert.start_witch_alphabet('0123') -- わざと失敗させる
  end)
  it("not begin alphabetic character", function()
    assert.not_start_witch_alphabet('h123') -- わざと失敗させる
  end)
end)

spy, mock, stub

spy,stub を使用すると関数に対して以下をテストできます

  • 関数が呼び出されたか
  • 何度関数が呼び出されたか
  • 関数呼び出し時に渡された引数の値は期待通りか

spy は対象の関数をそのまま呼び出しますが、stub の場合実際には関数が呼び出されません。
stub のサンプルを記載します。

describe("stubs", function()
  it("replaces an original function", function()
    local t = {
      greet = function(msg) print(msg) end
    }

    stub(t, "greet")

    t.greet("Hey!") -- DOES NOT print 'Hey!'
    assert.stub(t.greet).was.called_with("Hey!")

    t.greet:revert()  -- reverts the stub
    t.greet("Hey!") -- DOES print 'Hey!'
  end)
end)

mockspy, stub どちらにも切り替えられような関数のようです。ここは実際に関数を呼び出したいが、この先は呼び出したくない。。。というような spy から stub に切り替えたいようなケースで mock を使えるかもしれません(正直なところ mock をいつ使うのかよく分かっていない)

private関数

個人的には public な関数のテストで private な関数も使用されため不要と考えていますが、以下のような形で対応無理やりテストできます。

local my_module = {}
local private_element = {"this", "is", "private"}

function my_module:display()
  print(string.concat(private_element, " "))
end

-- export locals for test
if _TEST then
  -- setup test alias for private elements using a modified name
  my_module._private_element = private_element
end

return my_module

終わりに

busted についてはシンプルなテストツールで覚えることが少なく割と簡単に使い始めることが出来ました。是非使ってみてください。

また、冒頭でも触れましたが Neovim の Lua プラグインのテスト用に busted をラップした vusted というテストツールも存在します。Lua プラグインのテストを書きたい!という人は作者さんが zenn で vusted に関する記事記載されているので是非こちらも見てみてください。

https://zenn.dev/notomo/articles/neovim-lua-plugin-testing

ではでは、よいテストライフを❤

GitHubで編集を提案

Discussion