土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

rubyゲーム開発にユニットテスト/テスティングフレームワークを導入する(4)【モックの自動化(テストダブル)編】

 前回(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を作ると土屋にメンションをくれました(フットワーク軽いなリャン!)

https://twitter.com/the_zenspider/status/829609382128209920