vim-themisでパラメタライズドテストをする

# はじめに

最近のエンジニアは毎日Vim pluginを書き、公開していると聞きます。
そんな大ブームの中、Vim pluginを作成するにあたって重要な要素が一つあります。
そう、テストです！テスト！

Vim pluginをテストするにあたって便利なテスティングフレームワークはいくつかあります。

- [vader.vim](https://github.com/junegunn/vader.vim)
- [vim-vspec](https://github.com/kana/vim-vspec)
- [vim-themis](https://github.com/thinca/vim-themis)
- [vroom](https://github.com/google/vroom)
- etc...

今回はvim-themisを使った、便利なテストの書き方を紹介します。

- 注意
  - これは執筆段階では文書に記載されていない方法です。 
  - 執筆段階では、vim-themisのspec版の記述では、今回紹介する方法ができません。


また、今回はブラウザのフォントにより文字化けの恐れがあるので、hexでアイコンフォントを記載しています。
hexの値は以下のように読み返してください。

- \ue62b

![スクリーンショット 2021-12-06 17.19.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278618/81174f5f-3a7b-3f31-ff03-a4a23f52cce6.png)

- \ue612

![スクリーンショット 2021-12-06 17.29.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278618/f1b275f5-3a83-14ab-92a1-b2f26a2a4736.png)

> 引用元
> https://www.nerdfonts.com/cheat-sheet


# テストする対象
今回は、[vim-devicons](https://github.com/ryanoasis/vim-devicons)というプラグインを例に紹介します。

vim-deviconsには、`WebDeviconsGetFileTypeSymbol()`というグローバル関数が生えており、今回はこの関数の一部分をテスト対象にします。
以下に、テストを行う対象の仕様について記載します。

```
WebDeviconsGetFileTypeSymbol() は、アイコンを表すフォント文字列を返すAPIです。
以下のパラメータを受け取ります

- a:1（オプショナル）
    - ファイル名
- a:2（オプショナル）
    - ディレクトリかどうか（0 or 1）
```

今回は、`アーギュメントを1つ与えるケース（Vimのファイルアイコン取得）`についてのテストを例に取ります。

以下にアーギュメントを1つ与えるケース（Vimのファイルアイコン取得）の具体例を示します。

```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パターンについてのテストを記します。

```vim
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つで済みます。

```vim
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文を使えば、更に記述量を減らすことができます。

```vim
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をひとまとめに書いたテスト関数をすべて失敗するようにしてみます。

```vim
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件の失敗と認識されている
- テスト関数名がわかりにくい（どの条件（入力）が実行されたかわからない）

せっかくテストを書いたのに失敗時の情報量が少ないため、原因特定に時間がかかります。

## 効率的に、失敗時の情報が多いテストを書く

上記の方法が良くないことはわかったかと思います。
しかし、一番最初に紹介した方法は少し手間であることも事実です。
ここからは、効率的で、失敗時の結果がより多く得られるテストを紹介します。
これはパラメタライズドテストと呼ばれる、一つのテストロジックの入力パラメータを複数用意して実行し、複数のテストケースとして実行させる手法です。

まず最初にコードを示します。

```vim
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
```

### コードの解説

ここからは、上記のコードの解説をします。
上から順に見ていきましょう。

```vim
let targetfilenames = ['test.vim', '_vimrc', '.vimrc', 'vimrc', '_gvimrc', '.gvimrc', 'gvimrc']
let expecticon = "\ue62b"
```

`targetfilename`にはテストケースを、`expecticon`には期待される結果を入れています。

```vim
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`を定義しています。

```vim
  for targetfilename in targetfilenames
    let child[targetfilename] = funcref('s:Assert', [targetfilename, expecticon])
  endfor
```

`s:suite`もそうですが、`child`は辞書です。
この`child`のkeyにテストケース（ファイル名）、valueにテストのための関数（`s:Assert`）を指定します。
`funcref`は、定義した関数の参照を辞書に入れるために使用しています。

> funcrefの文書
> https://vim-jp.org/vimdoc-ja/eval.html#funcref()


`s:Assert()`の内容は下記のとおりです。

```vim
function! s:Assert(filename, icon)
  call s:assert.equals(WebDevIconsGetFileTypeSymbol(a:filename), a:icon)
endfunction
```

極端にいうと、以下のようなことを実現したかったのですが、このままでは動かないため、`funcref`等を使用して実現をした形になります。

```vim
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!**