14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

VimAdvent Calendar 2021

Day 7

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

Last updated at Posted at 2021-12-06

はじめに

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

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

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

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

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

  • \ue62b

スクリーンショット 2021-12-06 17.19.08.png

  • \ue612

スクリーンショット 2021-12-06 17.29.16.png

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

テストする対象

今回は、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は、定義した関数の参照を辞書に入れるために使用しています。

funcrefの文書
https://vim-jp.org/vimdoc-ja/eval.html#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!

14
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?