UTを使ってVim scriptのテストを書く

※追記:2012/4/1 自分でテスティングフレームワーク作成しました。
Vim scriptのテストを行うvimtestプラグインを書いた - ぼっち勉強会

最近Vim scriptの勉強を始めました。
:h script を見たりしながらプラグインを作成しようとしているのですが、
やはり何かを作るにはテストがないと安心できません。
テストなしに書き進められるほど、ぼくの実力は高くありません。
そこでVim scriptのテスティングフレームワークには何があるのか調べてみました。
結果としてUTが個人的に合っていたので使ってみることにしました。
その導入方法を簡単にですけど共有します。

テスティングフレームワークあれこれ

ググったりしてみると、以下のものが引っかかりました。

上記の中ではrunVimTestsとvimUnitを試してみました。

runVimTests

以下の理由から導入を辞めました。

  • 自分のMac OS X(10.7)で動かなかった
    • readlinkのところで「invalid option --f」となってしまいます。FreeBSDのreadlinkの挙動が考慮されていないのかもしれません。よくわかってません
  • なんかファイル用意するのが面倒だった&sh(bat)実行が面倒だった
    • やっぱ全部Vim内で完結するテストがいいなーと思って
      • VimShellから実行すれば完結するんですけど
  • okファイルとか用意するのが面倒に感じた
vimUnit
  • まだちゃんと出来上がっていないので却下
    • helpがTODOだらけだったので、まだ導入するものではないのかと思って
vspec

xSpec的に書けそうな予感で興味はあるんですが、使い方分からなかった!
あ、でもちゃんとヘルプを読んだ記憶はないからヘルプを読めば分かるのかも

UTをインストール

そんなわけでVim script自体よく分かっていないので、まずはテストも簡単に書けるのがいいんです。
そういう意味ではUTがいい感じでした。

Vundle管理の方は以下のようにしてvimrcに設定を書いて:BundleInstallです。

Bundle 'git://github.com/intuited/lh-vim-lib.git'
Bundle 'git://github.com/intuited/lh-vim-ut.git'

Vimballの方は本家からそれぞれ落としてきてインストールしましょう。
pathogenやNeoBundleは分かりません!

動作確認してみる

適当なところに次のようなsample_test.vimを作成します。

UTSuite [sample] sample 日本語も大丈夫

Assert 1 == 1

UTSuiteはテストスイートの定義だと思います。
「UTSuite [名前]説明」かと。
AssertはxUnitでいうところのassertEqualsっぽいです。

ファイルを保存してコマンド「:UTRun %」を実行してみましょう。
テストが実行されてQuickFixに結果が保存されています。
「:cw」としてQuickFixを開いて以下のように表示されていればOKです。

sample_test.vim|| SUITE <[sample] sample 日本語も大丈夫>
sample_test.vim|| SUITE<[sample] sample 日本語も大丈夫> 0/0 tests successfully executed.

テストを書いてみる

上記はただのAssert一つでした。
グローバルな箇所で記述することも可能ですが、
テスト関数を作らないと結果としてテストとは認識されません。
テスト関数はs:Test始まりもしくはs:test始まりじゃないとテストとして認識されないようです。
以下に簡単なサンプルを書いてみます。

UTSuite [sample] sample 日本語も大丈夫

function! s:Test1()
  Assert 1 == 1
endfunction

:UTRun %

sample_test.vim|| SUITE <[sample] sample 日本語も大丈夫>
sample_test.vim|| SUITE<[sample] sample 日本語も大丈夫> 1/1 tests successfully executed.

ちゃんと「1/1 tests successfully executed.」としてテストが1件と認識されたようです。

失敗するテストを含めてみる

UTSuite [sample] sample 日本語も大丈夫

function! s:TestOK()
  Assert 1 == 1
endfunction

function! s:TestNG()
  Assert 1 == 2
endfunction

:UTRun %

sample_test.vim|| SUITE <[sample] sample 日本語も大丈夫>
sample_test.vim|8| assertion failed: 1 == 2
sample_test.vim|| SUITE<[sample] sample 日本語も大丈夫> 1/2 tests successfully executed.

失敗したテストのAssertが簡潔な内容で表示されました。
ここは、もうちょっと詳細なメッセージがあると嬉しいですね。

失敗したあとのテストは走るの?

UTSuite [sample] sample 日本語も大丈夫

function! s:Test1()
  Assert 1 == 1
endfunction

function! s:Test2()
  Assert 1 == 2
endfunction

function! s:Test3()
  Assert 1 == 1
endfunction

:UTRun %

sample_test.vim|| SUITE <[sample] sample 日本語も大丈夫>
sample_test.vim|8| assertion failed: 1 == 2
sample_test.vim|| SUITE<[sample] sample 日本語も大丈夫> 2/3 tests successfully executed.

走っています。s:Test2で失敗したにも関わらずTest3が実行されて、
結果として「2/3 tests successfully executed.」となりました。

AssertとAssert!の違いは?

AssertではなくAssert!というのもあります。
これは失敗したらそのあとは実行しないよということだとヘルプにありました。

UTSuite [sample] sample 日本語も大丈夫

function! s:Test()
  echo "hey"
  Assert 1 == 1
  echo "heyhey"
  Assert 1 == 2
  echo "heyheyhey"
endfunction

:UTRun %

hey
heyhey
heyheyhey
UTSuite [sample] sample 日本語も大丈夫

function! s:Test()
  echo "hey"
  Assert 1 == 1
  echo "heyhey"
  Assert! 1 == 2 "ここで失敗するからあとは実行しない
  echo "heyheyhey"
endfunction

:UTRun %

hey
heyhey

とはいえ、そのテスト中の残りを実行しないということであって、
残りのs:Test関数を実行しない訳ではありません。

UTSuite [sample] sample 日本語も大丈夫

function! s:Test1()
  Assert 1 == 1
endfunction

function! s:Test2()
  Assert! 1 == 2
endfunction

function! s:Test3()
  Assert 1 == 1
endfunction
workspace/vimscript/sample_test.vim|| SUITE <[sample] sample 日本語も大丈夫>
workspace/vimscript/sample_test.vim|8| assertion failed: 1 == 2
workspace/vimscript/sample_test.vim|8| Test <Test2> execution aborted on critical assertion failure
workspace/vimscript/sample_test.vim|| SUITE<[sample] sample 日本語も大丈夫> 2/3 tests successfully executed.

簡単なhello関数とテストを書くとこんな感じになりました。

function! s:hello(...)
  let greet = "hello"
  return a:0 > 0 ? greet." ".a:1 : greet
endfunction

UTSuite [Hello] 引数なし
function! s:TestHello()
  Assert s:hello() == "hello"
endfunction

UTSuite [Hello] 引数あり
function! s:TestHello()
  let arg = "kanno"
  Assert s:hello(arg) == "hello " . arg
endfunction

でもコマンド打ったりQuickFix開いたりするのは面倒だよ!

UTとは直接関係ないけど、設定を少し変えてより使いやすくしましょう!

QuickFixを自動で開く

以下の設定をvimrcに追記します。

autocmd QuickfixCmdPost make,grep,grepadd,vimgrep if len(getqflist()) != 0 | copen | endif
テストを保存時に自動実行する

以下の設定をvimrcに追記します。

autocmd BufWritePost *_test.vim execute ":UTRun %"

ファイルパターンはお好みで。

おまけ。注意(自分がハマった)

function! s:hello(...)
endfunction

UTSuite [Hello] 引数なし
function! s:TestHello()
  Assert s:hello() == "hello"
endfunction

これを実行するとどうなるか分かるでしょうか。
そうです、成功します。

sample_test.vim|| SUITE <[Hello] 引数なし>
sample_test.vim|| SUITE<[Hello] 引数なし> 1/1 tests successfully executed.

Vim scriptでは空の関数は0を返します。

" :h 41.7
関数が ":endfunction" まで実行されたとき、あるいは引数無しで ":return" を使っ
たときは 0 が返ります。

また、数値と文字列の比較では文字列が数値に変換されます。文字列が数値でない場合は0になります。

" :h 41.4
文字列と数値を比較するときは、文字列を数値に変換します。文字列が数字ではなかっ
たときは 0 になるので注意してください。例: >

つまり、
s:hello()=>空の関数=>0
"hello"=>数値ではない文字列=>0
0 == 0 =>true
となりパスしてしまうのでした。
やってくれるぜ(ただの勉強不足)

UTのヘルプを見る

:h UT.txt です。

その他の機能とか

:h UT.txt です。

こうなるとさらにいいんだけどなあ

failedの詳細が知りたい

エラー時のメッセージが少なさすぎます。

ちゃんと動いているか不安なところもあり

次のようなテストを書きました。
固定で"hello"を返すので、引数を渡されても期待通りにはいきません。

function! s:hello(...)
  return "hello"
endfunction

UTSuite [Hello] 引数なし
function! s:TestHello()
  Assert s:hello() == "hello"
endfunction

UTSuite [Hello] 引数あり
function! s:TestHello()
  let arg = "kanno"
  Assert s:hello(arg) == "hello " . arg
endfunction
sample_test.vim|| SUITE <[Hello] 引数なし>
sample_test.vim|| SUITE <[Hello] 引数あり>
sample_test.vim|13| assertion failed: s:hello(arg) == "hello " . arg
sample_test.vim|13| assertion failed: s:hello(arg) == "hello " . arg
sample_test.vim|| SUITE<[Hello] 引数あり> 0/2 tests successfully executed.

実行してみると「0/2 成功」とはおかしい。
一つ目のテストは通るはず。
その証拠に、二つ目のテストを削除するとパスします。

function! s:hello(...)
  return "hello"
endfunction

UTSuite [Hello] 引数なし
function! s:TestHello()
  Assert s:hello() == "hello"
endfunction
sample_test.vim|| SUITE <[Hello] 引数なし>
sample_test.vim|| SUITE<[Hello] 引数なし> 1/1 tests successfully executed.

今後に期待?

一年以上も更新されていませんので「期待」は出来なさそうです。
上記にあげた誤作動があるのですが、それでもまあ使いやすい。
Vim scriptの最初の練習にはよさそうです。

とはいえ完全には満足していません。
これはつまり自分でプラグインを作るチャンス、不満は需要の裏返しです。
不満を自分で解消できるように、これからUTを使ってVim scriptを学んでいきます。