vimtest修正記録-Vim scriptテスト用にモックのサンプルを書いてみた

vimtestについてはこちらVim scriptのテストを行うvimtestプラグインを書いた - ぼっち勉強会
モチベーション維持と振り返りのための記録

  • モック出来るかも?

追記:最新版vimtest修正記録-Vim scriptテスト用にモック機能を書いてみたpart2 - ぼっち勉強会

モック出来るかも?

Big Sky :: vimでスクリプト内関数を書き換える
この記事を読んで、モック機能が出来るんじゃないかと思って勢いで書いてみた。
まだ雛形なので、アレコレ手直ししないといけないけどとりあえず。

  • 呼び出し例
function! s:foo(text)
  return  "Hello, " . a:text
endfunction

function! s:bar(text)
  call g:mock.called()
  return  "GoodNight, " . a:text
endfunction

echo s:foo("World")
" => Hello, World

let g:mock = s:get_mock(expand("%:p"))
call g:mock.once().method('foo').will('bar')
echo s:foo("World")
" => GoodNight, World
call g:mock.assert()

sourceして実行してみる。

Hello, World
GoodNight, World

この場合は「一回だけ(once)」という挙動に合うので何も言われない。
mattnさんの記事にある通り処理内容も変わっている。
これを、

let g:mock = s:get_mock(expand("%:p"))
call g:mock.once().method('foo').will('bar')
echo s:foo("World")
echo s:foo("World")
echo s:foo("World")
call g:mock.assert()

のように複数回呼ぶと「期待している数と違うよ」と怒られる。

Error detected while processing function 499:
line 2:
Function foo is expected 1 bat was 3

「1回って言ったのに3回も呼んでるじゃん!」

  • イケてない・気になる部分
    • 呼び出し回数を記録するために「call g:mock.called()」ってコードが必要になってる
      • そのためにグローバルにモックオブジェクトが置かれてしまう
    • call g:mock.assert()の記述が必要になってる
      • これはvimtest側で隠蔽できそうだから大丈夫かな
    • 期待する挙動のための関数(例ではbar)を定義しないと使えない
    • with的な引数の検証どうしよう
  • モックオブジェクト作成部分
function! s:get_mock(fname)
  let mock = {
        \ 'fname' : a:fname,
        \ '_old'  : '',
        \ '_new'  : '',
        \ '_called_actual'    : 0,
        \ '_called_expected'  : 0,
        \}
  function! mock.once()
    let self._called_expected = 1
    return self
  endfunction
  function! mock.method(funcname)
    let self._old = a:funcname
    return self
  endfunction
  function! mock.will(funcname)
    let self._new = a:funcname
    " 上書きしちゃうと影響ありそうなので最終的には戻すなどの対応が必要
    call s:hook_func(s:get_func(self.fname, self._old), s:get_func(self.fname, self._new))
    return self
  endfunction
  function! mock.assert()
    if self._called_actual != self._called_expected
      echoerr printf('Function %s is expected %s bat was %s', self._old, self._called_expected, self._called_actual)
    endif
  endfunction
  function! mock.called()
    let self._called_actual += 1
    return self
  endfunction
  return mock
endfunction
  • コード全部
" @see http://mattn.kaoriya.net/software/vim/20090826003359.htm
function! s:get_sid(fname)
  let snlist = ''
  redir => snlist
  silent! scriptnames
  redir END
  let smap = {}
  let mx = '^\s*\(\d\+\):\s*\(.*\)$'
  for line in split(snlist, "\n")
    let smap[tolower(substitute(line, mx, '\2', ''))] = substitute(line, mx, '\1', '')
  endfor
  return smap[tolower(a:fname)]
endfunction

function! s:get_func(fname, funcname)
  let sid = s:get_sid(a:fname)
  return function("<SNR>".sid."_".a:funcname)
endfunction

function! s:hook_func(funcA, funcB)
  if type(a:funcA) == 2
    let funcA = substitute(string(a:funcA), "^function('\\(.*\\)')$", '\1', '')
  else
    let funcA = a:funcA
  endif
  if type(a:funcB) == 2
    let funcB = substitute(string(a:funcB), "^function('\\(.*\\)')$", '\1', '')
  else
    let funcB = a:funcB
  endif
  let oldfunc = ''
  redir => oldfunc
  silent! exec "function ".funcA
  redir END
  let g:hoge = oldfunc
  exec "function! ".funcA."(...)\nreturn call('" . funcB . "', a:000)\nendfunction"
endfunction
" ------------------
function! s:get_mock(fname)
  let mock = {
        \ 'fname' : a:fname,
        \ '_old'  : '',
        \ '_new'  : '',
        \ '_called_actual'    : 0,
        \ '_called_expected'  : 0,
        \}
  function! mock.once()
    let self._called_expected = 1
    return self
  endfunction
  function! mock.method(funcname)
    let self._old = a:funcname
    return self
  endfunction
  function! mock.will(funcname)
    let self._new = a:funcname
    " 上書きしちゃうと影響ありそうなので最終的には戻すなどの対応が必要
    call s:hook_func(s:get_func(self.fname, self._old), s:get_func(self.fname, self._new))
    return self
  endfunction
  function! mock.assert()
    if self._called_actual != self._called_expected
      echoerr printf('Function %s is expected %s bat was %s', self._old, self._called_expected, self._called_actual)
    endif
  endfunction
  function! mock.called()
    let self._called_actual += 1
    return self
  endfunction
  return mock
endfunction
" ------------------
function! s:foo(text)
  return  "Hello, " . a:text
endfunction

function! s:bar(text)
  call g:mock.called()
  return  "GoodNight, " . a:text
endfunction

echo s:foo("World")
" => Hello, World

let g:mock = s:get_mock(expand("%:p"))
call g:mock.once().method('foo').will('bar')
echo s:foo("World")
" => GoodNight, World
call g:mock.assert()