はじめに
最近のエンジニアは毎日Vim pluginを書き、公開していると聞きます。
そんな大ブームの中、Vim pluginを作成するにあたって重要な要素が一つあります。
そう、テストです!テスト!
Vim pluginをテストするにあたって便利なテスティングフレームワークはいくつかあります。
- vader.vim
- vim-vspec
- vim-themis
- vroom
- etc...
今回はvim-themisを使った、便利なテストの書き方を紹介します。
- 注意
- これは執筆段階では文書に記載されていない方法です。
- 執筆段階では、vim-themisのspec版の記述では、今回紹介する方法ができません。
また、今回はブラウザのフォントにより文字化けの恐れがあるので、hexでアイコンフォントを記載しています。
hexの値は以下のように読み返してください。
- \ue62b
- \ue612
テストする対象
今回は、vim-deviconsというプラグインを例に紹介します。
vim-deviconsには、WebDeviconsGetFileTypeSymbol()
というグローバル関数が生えており、今回はこの関数の一部分をテスト対象にします。
以下に、テストを行う対象の仕様について記載します。
WebDeviconsGetFileTypeSymbol() は、アイコンを表すフォント文字列を返すAPIです。
以下のパラメータを受け取ります
- a:1(オプショナル)
- ファイル名
- a:2(オプショナル)
- ディレクトリかどうか(0 or 1)
今回は、アーギュメントを1つ与えるケース(Vimのファイルアイコン取得)
についてのテストを例に取ります。
以下にアーギュメントを1つ与えるケース(Vimのファイルアイコン取得)の具体例を示します。
call WebDevIconsGetFileTypeSymbol("test.vim")
" -> "\ue62b" を返します
" NOTE: ドットを含む文字列が与えられた場合ドット以下の拡張子によって適切なフォント文字列を返します
基本的なテストを書く
Vimのアイコンを取得することができるファイル名は以下の7種類です
- vimrc
- .vimrc
- _vimrc
- gvimrc
- .gvimrc
- _gvimrc
- xxx.vim(xxxは任意の命名)
NOTE: vimrcの種類について
https://vim-jp.org/vimdoc-ja/starting.html#vimrc
今回は、この6パターンについてのテストを記します。
let s:suite = themis#suite('WebDevIconsGetFileTypeSymbol')
let s:assert = themis#helper('assert')
function! s:suite.WebDevIconsGetFileTypeSymbol_testdotvim_returnVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('test.vim'), "\ue62b")
endfunction
function! s:suite.WebDevIconsGetFileTypeSymbol_vimrc_returnVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('vimrc'), "\ue62b")
endfunction
function! s:suite.WebDevIconsGetFileTypeSymbol_gvimrc_returnVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('gvimrc'), "\ue62b")
endfunction
function! s:suite.WebDevIconsGetFileTypeSymbol_dotvimrc_returnVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('.vimrc'), "\ue62b")
endfunction
function! s:suite.WebDevIconsGetFileTypeSymbol_dotgvimrc_returnVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('.gvimrc'), "\ue62b")
endfunction
function! s:suite.WebDevIconsGetFileTypeSymbol_underbarvimrc_returnVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('_vimrc'), "\ue62b")
endfunction
function! s:suite.WebDevIconsGetFileTypeSymbol_underbargvimrc_returnVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('_gvimrc'), "\ue62b")
endfunction
基本的に、1テストケースに1テスト関数なので、このような内容になります。
テスト関数には、以下のような条件で名前をつけています。
テスト対象関数名_テスト対象に与える条件(入力)_期待する結果(出力)
テスト結果
themis test --reporter spec
を実行した結果を以下に示します。
WebDevIconsGetFileTypeSymbol
[✓] WebDevIconsGetFileTypeSymbol_testdotvim_returnVimIcon
[✓] WebDevIconsGetFileTypeSymbol_vimrc_returnVimIcon
[✓] WebDevIconsGetFileTypeSymbol_gvimrc_returnVimIcon
[✓] WebDevIconsGetFileTypeSymbol_dotvimrc_returnVimIcon
[✓] WebDevIconsGetFileTypeSymbol_dotgvimrc_returnVimIcon
[✓] WebDevIconsGetFileTypeSymbol_underbarvimrc_returnVimIcon
[✓] WebDevIconsGetFileTypeSymbol_underbargvimrc_returnVimIcon
tests 7
passes 7
もっと効率的にテストする
上記のテストを見たときに、冗長だなぁと思った人もいるかも知れません。
例えば、下記のようにテストを書けばテスト関数は1つで済みます。
let s:suite = themis#suite('WebDevIconsGetFileTypeSymbol')
let s:assert = themis#helper('assert')
function! s:suite.OneArgumet_GetVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('test.vim'), "\ue62b")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('vimrc'), "\ue62b")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('gvimrc'), "\ue62b")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('.vimrc'), "\ue62b")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('.gvimrc'), "\ue62b")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('_vimrc'), "\ue62b")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('_gvimrc'), "\ue62b")
endfunction
上記のテスト関数にfor文を使えば、更に記述量を減らすことができます。
let s:suite = themis#suite('WebDevIconsGetFileTypeSymbol')
let s:assert = themis#helper('assert')
function! s:suite.OneArgumet_GetVimIcon()
let targetfilenames = ['test.vim', '_vimrc', '.vimrc', 'vimrc', '_gvimrc', '.gvimrc', 'gvimrc']
for targetfilename in targetfilenames
call s:assert.equals(WebDevIconsGetFileTypeSymbol(targetfilename), "\ue62b")
endfor
endfunction
しかし、このテストの書き方では問題があります。
極端な例ですが、assertをひとまとめに書いたテスト関数をすべて失敗するようにしてみます。
let s:suite = themis#suite('WebDevIconsGetFileTypeSymbol')
let s:assert = themis#helper('assert')
function! s:suite.OneArgumet_GetVimIcon()
call s:assert.equals(WebDevIconsGetFileTypeSymbol('test.vim'), "")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('vimrc'), "")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('gvimrc'), "")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('.vimrc'), "")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('.gvimrc'), "")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('_vimrc'), "")
call s:assert.equals(WebDevIconsGetFileTypeSymbol('_gvimrc'), "")
endfunction
テスト結果
WebDevIconsGetFileTypeSymbol
[✖] OneArgumet_GetVimIcon
The equivalent values were expected, but it was not the case.
expected: ""
got: "\ue612" (本当はアイコンフォントが出てきますが、文字化けを避けるためにhexを記載しています)
tests 1
passes 0
fails 1
- NOTE: for文のケースでも同じ実行結果になります。
テスト結果を見ると、以下のような問題があります。
- テストに失敗したことはわかるが、どのテストケースが失敗しているかわからない
- 7件失敗しているはずだが、1件の失敗と認識されている
- テスト関数名がわかりにくい(どの条件(入力)が実行されたかわからない)
せっかくテストを書いたのに失敗時の情報量が少ないため、原因特定に時間がかかります。
効率的に、失敗時の情報が多いテストを書く
上記の方法が良くないことはわかったかと思います。
しかし、一番最初に紹介した方法は少し手間であることも事実です。
ここからは、効率的で、失敗時の結果がより多く得られるテストを紹介します。
これはパラメタライズドテストと呼ばれる、一つのテストロジックの入力パラメータを複数用意して実行し、複数のテストケースとして実行させる手法です。
まず最初にコードを示します。
let s:suite = themis#suite('WebDevIconsGetFileTypeSymbol')
let s:assert = themis#helper('assert')
function! s:Assert(filename, icon)
call s:assert.equals(WebDevIconsGetFileTypeSymbol(a:filename), a:icon)
endfunction
function! s:suite.__OneArgument_VimIcon__()
let targetfilenames = ['test.vim', '_vimrc', '.vimrc', 'vimrc', '_gvimrc', '.gvimrc', 'gvimrc']
let expecticon = "\ue62b"
let child = themis#suite('OneArgument_VimIcon')
for targetfilename in targetfilenames
let child[targetfilename] = funcref('s:Assert', [targetfilename, expecticon])
endfor
endfunction
コードの解説
ここからは、上記のコードの解説をします。
上から順に見ていきましょう。
let targetfilenames = ['test.vim', '_vimrc', '.vimrc', 'vimrc', '_gvimrc', '.gvimrc', 'gvimrc']
let expecticon = "\ue62b"
targetfilename
にはテストケースを、expecticon
には期待される結果を入れています。
function! s:suite.__OneArgument_VimIcon__()
let child = themis#suite('OneArgument_VimIcon')
endfunction
これはvim-themisのchild機能です。これはテストをネストするために使っています。ドキュメントには以下のように記載されています。
(一部抜粋)
- __XXX__()
- XXX is any string.
- This is called before the all test.
- You can make nested suite in this function. >
let s:parent = themis#suite('parent')
function s:parent.__child__()
let child = themis#suite('child')
function child.test()
" Test code here...
endfunction
endfunction
私が書いたテスト関数では、s:suite
が親のテスト関数となっており、その子関数として、child
を定義しています。
for targetfilename in targetfilenames
let child[targetfilename] = funcref('s:Assert', [targetfilename, expecticon])
endfor
s:suite
もそうですが、child
は辞書です。
このchild
のkeyにテストケース(ファイル名)、valueにテストのための関数(s:Assert
)を指定します。
funcref
は、定義した関数の参照を辞書に入れるために使用しています。
s:Assert()
の内容は下記のとおりです。
function! s:Assert(filename, icon)
call s:assert.equals(WebDevIconsGetFileTypeSymbol(a:filename), a:icon)
endfunction
極端にいうと、以下のようなことを実現したかったのですが、このままでは動かないため、funcref
等を使用して実現をした形になります。
for targetfilename in targetfilenames
function child.targetname()
call s:assert.equals(WebDevIconsGetFileTypeSymbol(targetfilename), "\ue62b")
endfunction
endfor
テスト結果
実際にテストを実行すると、以下の結果を得られます。
WebDevIconsGetFileTypeSymbol
OneArgument_VimIcon
[✓] gvimrc
[✓] .vimrc
[✓] _gvimrc
[✓] _vimrc
[✓] test.vim
[✓] vimrc
[✓] .gvimrc
tests 7
passes 7
2つ失敗するケースを入れた結果も以下に記します。
WebDevIconsGetFileTypeSymbol
OneArgument_VimIcon
[✓] gvimrc
[✖] 失敗するファイル1
The equivalent values were expected, but it was not the case.
expected: "\ue62b"
got: "\ue612"
[✖] 失敗するファイル2
The equivalent values were expected, but it was not the case.
expected: "\ue62b"
got: "\ue612"
[✓] .vimrc
[✓] _gvimrc
[✓] _vimrc
[✓] test.vim
[✓] vimrc
[✓] .gvimrc
tests 9
passes 7
fails 2
これなら、どのテストケースで失敗したのか、ひと目で分かりますね!
また、テストケースごとに期待する結果と実際の結果がわかります。
これで、問題を正確に、見やすく検知することが可能になります。
最後に
いかがだったでしょうか?
vim-themisを使ってパラメタライズドテストを実現することができました。
もし気になった方は是非試して見てください。
:qall!