技術書典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サービスで動けばもうなんでもできるな!(ホントかよ)