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

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

rubyゲーム開発にユニットテスト/テスティングフレームワークを導入する(3)【I/Oモックの作成編】

 技術書典2に合わせて、以前書いたゲーム開発にユニットテストを導入する手法についてまとめた本を作るつもりでいます。追加として「I/Oモックの作成」「CIサービスへの登録」までできればいいなあと思っています(CIサービスまでは間に合わないかもしれない)。今回はI/Oモックについて。

I/Oモックとはなにか

 ゲームプログラムのユニットテストを行う際には、ネックとなる部分が幾つか(あるいは大量に)ありまして、その中の一つに「ユーザーのキー入力に応じて行われる処理のテスト」があります。テストを実行した人が、実際に必要なキー入力を行い、その結果を確認すればいいのですが、これだと自動テストになりません。

 こういう時は、キー入力処理をラップし、実際のI/Oの代わりに必要な値を返すモックオブジェクトを用意します。司エンジンではキー入力にDXRuby::Inputを使っているので、これのラッパークラスを用意し、テスト時のみラッパークラスの方を使用する形にします。v2.2からこの機構が組み込まれます。

コードサンプル

 2.2で実際に動作するテストコードはこちらになります。

MiniTest.autorun

#テスト用DXRuby::Inputエミュレートクラス
class TestInput
  #DXRuby::Input.key_down?をフックする……※②
  def self.key_down?(pad_code)
    #引数でK_Z(Zキー)が指定された場合はtrueを返す
    if pad_code == Tsukasa::K_Z
      return true
    else
      return false
    end
  end

  #フックされていないメソッドについてはDXRuby::Inputのメソッドをそのまま渡す
  def self.method_missing(command_name, options)
    return DXRuby::Input.send(command_name, options)
  end
end

class TestInputBase < Minitest::Test
  #ゲーム側で判定タイミングのトリガーを用意するテスト
  def test_2017_01_09_1_キー入力確認
    puts "zキーを押してください"
    #コントロールの生成
    control = Tsukasa::Control.new() do
      #動的プロパティの追加
      _DEFINE_PROPERTY_ test: nil
      #無限ループ
      _LOOP_ do
        #Inputオブジェクトをモックのクラスを指定して生成……※①
        _CREATE_ :Input, id: :input, _INPUT_API_: TestInput
        #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

 2箇所だけ解説しておきます。

#Inputオブジェクトをモックのクラスを指定して生成……※①
_CREATE_ :Input, id: :input, _INPUT_API_: TestInput

 Inputコントロールを生成する際、_INPUT_API_オプションにDXRuby::Input互換クラスを指定すると、キー入力判定時にそちらのクラスを実行します。省略時(つまり、通常時)はDXRuby::Inputが使用されます。

  #DXRuby::Input.key_down?をフックする……※②
  def self.key_down?(pad_code)
    #引数でK_Z(Zキー)が指定された場合はtrueを返す
    if pad_code == Tsukasa::K_Z
      return true
    else
      return false
    end
  end

 モック側では想定される値を返します。このコードでは固定値になっていますが、Dataコントロールなどの値を見て、任意のタイミングで必要な値を返せます。現状ではまだちょっと使いにくいので、もっと簡単にキー入力をエミュレートする方法を考えようと思っています。
 DXRuby::Input以外にもモックを作れるようにするかは現在検討中です。DXRuby::Windowとかは作れるけど、あんまり意味ないかな……?

NEXT

 想像していたよりサクっと組めてしまって実は驚いています。あとはCIサービスで動けばもうなんでもできるな!(ホントかよ)