前回(http://d.hatena.ne.jp/t_tutiya/20170205/1486285294)、キー入力を管理するオブジェクト(正確にはモジュール)をエミュレートするモックオブジェクトを作成し、それを差し替えることで、実際にキー入力を行わなくても、同等のテストが可能であることを確認しました。
多少込み入ったユニットテストを行う場合、こういうモックオブジェクトの作成が頻繁に起こります。毎回フルスクラッチするのは面倒なので自動化を支援する機能を使うことにします。このような機能を総称してテストダブルと言います。
テストダブル(via wikipedia)
https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3%83%AB
ちなみにこれはスタントダブル(役者そっくりの格好をさせて画面に映るスタントマンのこと)のもじりかと思います。
最初はRubyの有名なテストダブルフレームワークである"rr(http://rr.github.io/rr/)"を使うつもりだったのですが、ソースコードを見る限りどうもMinitest内で使用するのは非推奨ぽかったので、今回はMinitest自身が提供しているライブラリMinitest::Mockを使うことにしました。
前回のコードをMinitest::Mockに置き換えたのが以下になります。自前のモックオブジェクトがなくなり、Input#key_down?だけをスタブ化(特定のメソッドが呼びだされたときにその動きをフックすること)しています。また、1フレームごとに返す値を明示できるのはテストとして使いやすいかもしれません。
#ゲーム側で判定タイミングのトリガーを用意するテスト def test_2017_02_08_1_キー入力確認_minitest_mock #Procのような物を生成する test_module_mock = MiniTest::Mock.new #Input.key_down?(Tsukasa::K_Z)が実行された時に返す真偽値を設定する #1フレーム目:false test_module_mock.expect :call, false, [Tsukasa::K_Z] #2フレーム目:false test_module_mock.expect :call, false, [Tsukasa::K_Z] #3フレーム目:true←ここで_CHECK_が成立する test_module_mock.expect :call, true, [Tsukasa::K_Z] #スタブを設定する DXRuby::Input.stub(:key_down?, test_module_mock) do #このブロック内でInput.key_down?が実行されるとtest_module_mock#callが呼びだされる puts "zキーを押してください" #コントロールの生成 control = Tsukasa::Control.new() do #動的プロパティの追加 _DEFINE_PROPERTY_ test: nil #無限ループ _LOOP_ do #Inputオブジェクトをモックのクラスを指定して生成 _CREATE_ :Input, id: :input #zキーが押された場合 _CHECK_ [:_ROOT_, :input], key_down: Tsukasa::K_Z do #プロパティに値を設定 _SET_ test: Tsukasa::K_Z #メインループを終了する _EXIT_ end #1フレ送る _HALT_ end end #メインループ DXRuby::Window.loop() do control.update(DXRuby::Input.mouse_x, DXRuby::Input.mouse_y) #処理 control.render(0, 0, DXRuby::Window) #描画 break if control.exit #メインループ終了判定 end assert_equal(control.test, Tsukasa::K_Z) end #テスト end
ただ、テストコードとして分かりやすいのか? というのは正直疑問です。Minitestのモックは実装がミニマムなため、凝ったことをするとコードが複雑になる傾向があるようです。また、stubのようなメソッドをを、MinitestがObjectを直接拡張して実現しているのも、ちょっとやりすぎではないかと感じました。
次はRubyにおけるテストフレームワークのデファクトスタンダードであるRSpecを使ってみる予定です。
補足:":call"とはなんぞや、という話(知りたい人向けのマニアックな話)
今回やってみたモジュールメソッドにstubを設定する方法については、こちらの記事を参考にさせてもらいました。
minitestでモジュールメソッドにstubを使う
http://qiita.com/matsukaz/items/6616d6f18e98108e9207
上のコードにおいて、スタブのブロック内でInput.key_down?が実行されると、スタブが機能してtest_module_mock#callが呼ばれ、それによってexpectした結果が返るのですが、そもそもこのcallってなんなのでしょうか?
つまる所これはProc#callなのです。上記ではわざわざMiniTest::Mockを生成していますが、stubの引数に直接Procを設定しても同じように動作するわけです。むしろ、本来はProc#callが呼ばれるべき箇所を、こちらが無理矢理ハックしていると言った方が正確かもしれません。
実際、Minitestの作者であるリャン・デイビスは、こういう時は直接Procを作ると土屋にメンションをくれました(フットワーク軽いなリャン!)