Vimマスターへの道:画面分割とターミナル機能でTDD促進

0
509

早くも「Vimマスターへの道」第二弾です。
日常的にVimを使用している私にとってVimに関する技術向上はそのまま仕事全体の品質向上、生産性アップに繋がるので、どうしても力が入ります。

今回は先に若干予告気味に触れてあった画面分割に関して整理したいと思います。
なお、本来Vim自体の利用は「手段」であって「目的」ではありません。Vimの使い方に関して記事をまとめる際には結果として期待する効果(実益)に関しても踏まえた内容にしたいと思っています。
今回画面分割を取り上げた背景としては当然ながら複数ファイルを開いての編集の必要性があり、それ自体は一般的によくあることなのですが、特にTDDにおいてはテストケースと被テストメソッドの間を行き来して作り込みを進めていく形になるため、その辺の操作性が重要になります。
と言うことで、Vimが提供する画面分割機能がTDDの効率化にどのように貢献しそうかと言う観点で書いて行きたいと思います。

なお、このテーマで調査を進めていく中で、Vim内から外部のコマンド類を実行できる「ターミナル機能」なるものがあり、これがなかなかの優れもの(単に外部コマンドが実行できるだけではない!)と言うことも分かったので、その辺も合わせて整理したいと思います。

TDDの作法

TDDを行う際には最低限以下の3つの操作が必要になります。

  1. テストケースを書く
  2. テストを実行する
  3. 被テストメソッドを書く

上記は若干「レッド・グリーン・リファクタリング」を意識した並びになっています。
実際の作業としては、

  1. テストケースを書く(ただしテストされるメソッド内の機能は未実装)
  2. テストを実行する(異常終了)
  3. 被テストメソッドを正しく書く
  4. テストを実行する(正常終了)

と言うフローをサイクルさせることになるでしょう。

では、上記3つの操作を今までどのように行っていたかと言いますと…実は3つのターミナルを別々に起動し、それぞれで上記3つの操作を行っていました。おかげで色々と面倒な点が。

  • 3つの画面のサイズや配置が今一つ気に入らない
    画面の横幅を大きくすると2つの画面(例えばテストケースと被テストメソッド)を並べて見たい時に重なって邪魔だし、短くすると行が折り返して見難い(1行が長すぎると言う問題があるかもしれませんが…)
  • 他にもターミナルを立ち上げている時など、目的のターミナルを見つけ難い
  • テスト環境への移動を個別に行う必要がある
    Vagrant上にテスト環境がある場合、ターミナルごとにVagrant環境に移動し、仮想マシンにログインし、同マシン内のテスト環境に移動する、と言うことを個別に行う必要がある

上記のような煩わしさもありますが、そもそもTDDの習慣が完全に身についている訳でもないので、少し油断すると普通にメソッドから書き始めていたりします。この辺も含めてしっかりとTDDを進めるための環境作りを考えていて目をつけたのがVimの画面分割とターミナル機能でした。

画面分割は文字通り起動したVimの画面を分割し、それぞれで別のファイルを操作できます。
ターミナル機能では、画面を分割しつつ、片方は通常のターミナルと同様にシェルコマンドが実行できます。
これらを組み合わせることで、前述したような問題を解決したイケイケのTDD環境を構築するのが今回の目的です。

画面分割

まずは画面分割の操作に関わるコマンドをざっくり整理します。

:sp [ファイル名]画面を水平分割(ファイル名省略時は現在の画面と同じファイル)
:vs [ファイル名]画面を垂直分割(ファイル名省略時は現在の画面と同じファイル)
<C-r>|画面の幅を最大まで広げる
<C-w>_画面の高さを最大まで広げる
<C-w>=画面の幅・高さを均等に戻す
<C-w>j下の画面に移動
<C-w>k上の画面に移動
<C-w>h左の画面に移動
<C-w>l右の画面に移動

画面分割に関連する操作は上記以外にもありますが、とりあえず私が特に必要と思ったものは上記9点です。
特に画面サイズの変更に関わる操作に関しては、もっと細かな操作もありますが、個人的には

  • 1つの画面(ファイル)に関してできる限り多くの情報が見られるように表示する
  • 複数の画面(ファイル)を対比して見られるように表示する

の2つができれば良さそうだったので、これらを実現する操作方法を調査した結果、1つの画面を大きく表示する方法と複数の画面を均等に表示する方法の二種類くらいでとりあえずは良さそうとの結論になりました。

蛇足ながら、上記における「<C-w>」はコントロールキーとwキーを同時に押すことを意味しますが、この操作になかなか馴染めなかったので、私の.vimrcでは他のキーにマッピングしてあります。

入力補完

画面分割に関連して追加で触れておきたいのは「入力補完」に関してです。
挿入モードにおいて<C-n>もしくは<C-p>と入力すると、そこまでの入力内容に準じて候補を選択できるようになります。候補が複数ある場合はそれらがメニュー形式で表示され、<C-n>で下に、<C-p>で上に候補を変更していくことができます。
なお、選択中の候補が、入力していた箇所にも反映されているので、メニュー上の移動後に改めて「選択する」と言う操作は必要なく、そのまま入力を続ければ補完入力文字列の後ろに追加で入力した内容が反映されて行きます。候補が1つの場合はメニューの表示も行われず補完された状態と補完されていない状態が交互に切り替わるのみです。この補完操作の簡素さはかなり使い勝手が良いです。

この補完の候補をどこから取得してくるかは「complete」と言う変数で決まっていて、デフォルトは「.,w,b,u,t,i」になっています。
それぞれの意味は以下の通り。

.カレントバッファから
w別の画面内のバッファから
bバッファリスト内の、現在読み込まれている別のバッファから
uバッファリスト内の、現在読み込まれていない別のバッファから
tタグ補完
iカレントファイルとインクルードされるファイルから

後半意味不明なものもありますが、少なくとも同時に開いているファイル相互の内容は補完候補になります。
一方で、別々のVim(別ウインドウで起動したVim)で読み込んだファイルの内容は相互に補完の対象にならないことも確認できています。
最初に書いたように、今まではそれぞれのファイルを別々のVimで開いていた訳で、完全に入力補完の恩恵を捨てていました…
タイポを減らすためにも入力補完は適切に使って行きたいところです。

ターミナル機能

ターミナル機能の起動はコマンドモードで以下のように実行します。

:[ターミナル用画面の位置] term [オプション]

「ターミナル用画面の位置」に関しては以下のような指定ができます。

vert垂直分割した左側をターミナルとする
bo水平分割した下側をターミナルとする
top水平分割した上側をターミナルとする(デフォルト)

「垂直分割した右側をターミナルとする」方法があるかどうかは知りません(必要がないので追求していません)。

オプションとしてもいくつか指定できるのですが、個人的に必要性を感じるのは以下の2つのみです。

++rows=高さターミナル画面の高さを指定
++cols=幅ターミナル画面の幅を指定

一応上記のように紹介しましたが、実のところ個人的な好みとしては、ターミナル画面はVim画面全体の下に配置し、デフォルトの高さはそれほど必要としない、と言うことで以下のような操作一択です。

:bo term ++rows=10

また、ターミナル機能には「ターミナルジョブモード」と「ターミナルノーマルモード」の2つの状態があります。
初期状態はターミナルジョブモードです。

ターミナルジョブモード

ターミナルジョブモードは普通に(Vimと無関係に)ターミナルを操作しているのと同じ状態です。ただし、例外的に前述したモードの切り替えおよび画面操作はVimのルールに準じます。画面操作は「画面分割」で紹介した画面サイズの変更や画面移動、つまり「<C-w>+キー」で指定する操作になります。

また、ターミナルジョブモードでは通常のマッピングが無効になっている模様です。マッピングを行う場合にあくまでVim上で支障のないキーの組み合わせを選択しますが、それがターミナルにおいて無害である保証まではないことから、ある意味当然かもしれません。
一方で、ターミナル用のマッピング「tmap」「tnoremap」も用意されています。ただ、ターミナルの操作はVimに関係なく普段から行っていることなので、今更別のキーを割り当てて嬉しいこともあまりないかと思っています。強いて言えば、後述するターミナルノーマルモードへの切り替え操作が面倒(「N」に関しても実際の操作においては「Shift+n」なので、「Ctrl+w」「Shift+n」と言うように4つのキー操作が必要)なので、これを以下のようにマッピングしています。

tnoremap <esc> <C-w>N

ターミナルでエスケープキーを押すことはあまりなさそうな印象なので、とりあえず上記のようにして様子を見ています。

ターミナルノーマルモード

ターミナルジョブモードの説明でも少し触れましたが、ターミナルノーマルモードに切り替えるには以下のコマンドを実行します(前述のように個人的には別コマンドにマッピングしていますが)。

<C-w>N

ターミナルノーマルモードでは通常のノーマルモードと同等の操作が可能です。マッピングも有効です。コマンドモード、ビューモードへの切り替えも可能ですが挿入モードへの切り替えのみ行えません。
ターミナルノーマルモードは、それまでの同ターミナルでの操作結果を編集不可能なデータとしてVimに読み込んだ状態と考えると良いかもしれません。操作および出力の履歴をVimの機能を使って検索したりコピペできたりするのはなかなかに便利です。

なお、ターミナルノーマルモードからターミナルジョブモードに戻る操作は「i」もしくは「a」キー入力です。つまり通常のノーマルモードにおける挿入モードへの切り替え操作と同じです(同様に挿入モードへ切り替える「o」「O」などは単に使えないだけですが)。
つまり、ターミナル機能においてターミナルジョブモードは通常操作における挿入モードと同じ位置付けと考えると色々と辻褄が合ってくるような気がします。
前述したようにターミナル画面における出力を「過去のアウトプットの編集不可能なデータ」と見るならば、それ自体を変更することはできませんがデータを追加することは問題ない訳で、新規に操作結果を追加することになるターミナルジョブモードへの切り替え操作がノーマルモードにおける挿入モードへの切り替え操作と同じになっている点に妙に納得してしまいます。

そう考えると、ターミナルジョブモードに戻る操作を挿入モードからノーマルモードへ戻る操作であるエスケープキー入力に対応させたくなり、先に示したマッピングを行いたくなる訳ですが、いかがでしょうか?

画面構造

本来は図で示すと分かりやすいのですが、図を描くのが面倒なので文章のみで(以降で紹介する手順を実際のVim上で実施してもらうのが早いかと)。
まず画面を水平分割し、下側をターミナルにします。前述したように初期段階では高さはあまり必要ないので低めに。
上側は垂直分割し、左でテストケース、右で被テストメソッドを含むファイルをそれぞれ開きます。
画面分割の仕方は本来自由ですが、本記事では上記構造を目指すと言うことです。

上記画面構造にするためには以下の操作をします。

1)被テストメソッドを含むファイルを開きます。

vi <被テストメソッドを含むファイル>

2)ターミナル機能を実行します

:bo term ++row=10

3)上記操作でターミナル機能を割り当てた画面(下側)に移動してしまっているので上の画面に戻ります

<C-w>k

4)画面を垂直分割しながらテストケースファイルを開きます

:vs <テストケースを含むファイル>

上記4つの手順で目的とした画面構造が実現できるはずです。

ターミナル画面は開いただけですが、ここでテストを実施して行きます。
先に示した分割画面間の移動操作でこれらの画面間の移動がキー操作のみで可能ですし、必要であればターミナル画面での出力結果をコピペして利用することなどもできます。

ユニットテスト環境

ここからはもう少し具体的な操作に触れていきたいと思いますが、前提としてはLaravelでの開発を想定します。

被テストメソッドはモデル(Eloquent)内に実装し、モデルの実体は「app/Models」配下に適当な名称でファイル化します。ファイル名とモデルのクラス名は合わせておきます。

ユニットテストに関するテストケースは標準的な環境である「tests/Unit」配下に作成します。なお、基本的には同環境に「xxxxTest.php」のような名称でファイルを作成する必要がありますが、あくまで個人的好みとして、クラスとしては「tests/Unit/Test.php」(class Test)を1つのみ用意し、個別テストケースは全てトレイトとして実装して「tests/Unit/Test.php」で読み込むようにしています(この辺に関しては過去記事「LaravelでTDD」を参照してください)。

また、被テストメソッドに対して1つのテストケースファイルを用意するようにします。例えばモデル「TUser」のメソッド「setData」に対してはテストケースファイル「tests/Unit/model_TUser_setData.php」が存在し、この中に同メソッドに関するテスト内容が書かれているようにします。
TDDにおけるテストケースは仕様書的な位置付けでもあり、テストケースが読める人にとっては、テストケースを見るだけで対象である処理の要件が分かるのが理想と言うような話も見聞きします。その域に達するのは相当な研鑽が必要かと思いますが、とりあえずテスト対象に対するテストケースが過不足なくまとまっている状態にすることが前述の構成の趣旨です。

さて、上記のようなファイルが存在する状況で「画面構成」で示したような環境を構築しようと思った場合、単純には「画面構成」で示した手順で操作をすれば良い訳ですが、せっかくなのでこの辺もVimのマッピングを使って簡略化したいと思います。

具体的には以下のような設定を行います。

nnoremap <Leader>lout ve"fy:let @l=line('.')<CR>:1/^class<CR>wve"my:<C-r>l<CR>:bo term ++rows=10<CR><C-w>k:vs tests/Unit/model_<C-r>m_<C-r>f.php<CR>

「<Leader>」に関しては「Vimマスターへの道:.vimrc」を参照してください。とりあえず「l(Laravel)o(Open)u(Unit)t(Test)」と言う操作にしてみました。この辺は完全に個人の好みなので、分かりやすさ、操作しやすさで決めれば良いかと。

上記操作の前提は、被テストメソッドを含むモデルのファイルをVimで開いておいて、被テストメソッド名の先頭にカーソルがある状態です。
この状態から以下の操作をバッチ的に実行します。

「v」ビジュアルモードに切り替え
「e」単語(つまりメソッド名)の最後まで移動(メソッド名全体が選択状態)
「”fy」レジスタfに選択範囲(メソッド名)を記録
「:let @l=line(‘.’)<CR>」現在の行数をレジスタlに記録
「:1」ファイルの先頭に移動
「/^class<CR>」行の先頭から「class」と書かれている行を検索し移動
「w」classの次の単語(つまりモデル名)に移動
「v」ビジュアルモードに切り替え
「e」単語(つまりモデル名)の最後まで移動(モデル名全体が選択状態)
「”my」レジスタmに選択範囲(モデル名)を記録
「:<C-r>l<CR>」先ほどレジスタlに記録した行に戻る
「:bo term ++rows=10<CR>」画面を水平分割した下側に高さ10行のターミナルを開く
「<C-w>k」上側の画面(モデル編集画面)に移動
「:vs tests/Unit/model_<C-r>m_<C-r>f.php<CR>」画面を垂直分割し、”tests/Unit/model_<モデル名>_<メソッド名>.php”ファイルを開く

実際、コマンド一発で想定した環境ができた時は、かなり嬉しいです。

機能テスト環境

機能(Feature)テストに関しても、要領はほぼ同じです。

被テストメソッドはコントローラ内に実装し、コントローラの実体は「app/Http/Controllers」配下に適当な名称でファイル化します。ファイル名とモデルのクラス名は合わせておきます。

テストケースはユニットテストと同様にクラスとしては「tests/Feature/Test.php」(class Test)を1つのみ用意し、個別テストケースは全てトレイトとして実装して「tests/Feature/Test.php」で読み込むようにします。

被テストメソッドに対して1つのテストケースファイルを用意するようにします。例えばモデル「UserController」のメソッド「entry」に対してはテストケースファイル「tests/Feature/controller_User_entry.php」が存在し、この中に同メソッドに関するテスト内容が書かれているようにします。
機能テスト単位がコントローラのメソッドと一致するかについては本来であれば怪しいところです。ただ、弊社の方針として今後はフロントエンドとバックエンドを分離し、バックエンド側はAPI経由で機能提供するのみ、フロントエンドはバックエンドのAPIをAjax(Axios等)で利用する形態を推進していこうとしています(蛇足ながらフロントエンド開発はNuxtを使用)。この構成であれば機能テスト単位=コントローラのメソッドで差し支えないと判断しています。

さて、上記状況に対してユニットテストと同様に環境構築用マッピングを設定します。

nnoremap <Leader>loft ve"fy:let @l=line('.')<CR>:1/^class<CR>wv/Controller<CR>h"my:<C-r>l<CR>:bo term ++rows=10<CR><C-w>k:vs tests/Feature/controller_<C-r>m_<C-r>f.php<CR>

コマンドは「l(Laravel)o(Open)f(Feature)t(Test)」にしました。

ユニットテストに関する設定と違う部分のみ触れておきます。

「/^class<CR>wv/Controller<CR>h”cy」クラス名(コントローラ名)を取得するが、コントローラ名は”xxxxController”のように逐一”Controller”が付加されていて冗長なので、これを省いた名称をコントローラ名としてレジスタcに記録
「:vs tests/Feature/controller_<C-r>c_<C-r>f.php<CR>」画面を垂直分割し、”tests/Feature/controller_<コントローラ名>_<メソッド名>.php”ファイルを開く

こちらも、コマンド一発で想定した環境ができると、かなり嬉しいです。

総括

ここまで書いておいて今更ですが、「TDDなら起点はテストケースなんじゃないの?」と言う疑問がなくもないです。
ただ、ユニットテスト/機能テストの対象がモデル/コントローラに限定される訳ではないですし、テストケース側のファイルに対して被テスト対象はメソッドであることなども考慮すると、テストケースから被テストメソッドのファイル内の位置なども考慮しつつファイルを開く流れは少々難易度が上がります。
また、あくまで個人的な好みになりますが、テストケースが左、被テストメソッドが右と言う画面配置の方が落ち着きがよく、(一応画面配置を変更する技もあるのですが)現在の手順が今のところのファイナルアンサーです。
まぁ、今回はあくまでVim修行の一環として画面分割とターミナル機能の紹介(調査・記録)が趣旨なので、この辺までとしておきます。

で、本題の画面分割とターミナル機能ですが、これはかなり使える印象です。
画面分割に関しては開発作業において必須と言っても良いです。特に入力補完に関しては別々にVimを起動している相互間では働かないことから、補完対象としたいファイルは画面分割で開くのが吉です。
ターミナル機能に関しても、普段から素のターミナルを使わずに、無条件にVim起動しておいてそのターミナル機能を使う形でも良いんじゃないか?くらいの勢いです。色々と使っていく中で粗が見えてくるかもしれませんが。

まだ使い出して間もないので正確なことは言えませんが、開発環境の利便性を大きく向上させる技なのではないかとの期待大です。