友達のスーパーエンジニアと飲んでいる時、「サンデーrubyプログラマである土屋が作っている司エンジンの次のステップアップとしてなにをすると良いか」と聞いたら「テストコードを書きなさい」と返事が来ました。はい。
というわけで、今後ちょっとずつテストコードを書いていこうと思います。さっそく今日躓いたので共有用のメモ。
ゲームにおけるユニットテスト
土屋がこれまでユニットテストを避けてきたのは、ゲームプログラミングの特徴である以下の二つが、ユニットテストと致命的に相性が合わないと考えていたからです。
1・コードが秒間60回呼びだされる(=メインループ処理)
2・ほとんどの場合、コードの実行結果が戻り値に反映されず、I/Oに出力される。
とはいえ、考えてみればRDBへの格納結果を比較するのだって事象とはしては同じなわけで、例えば現在のゲーム画面を取得して、予め保存していたスクリーンショットと比較することだってできるわけです。
実際やってみたら色々問題が出てくるとは思うのですが「テストしやすいように実装を再構築する」というのが取るべきアプローチとのことでした。
司エンジンにおけるユニットテスト
その後、スーパーエンジニアに司エンジンの実装を少し読んでもらい、スクリーンショットの比較までやらないで済むテスト方法のアイデアを貰い、以下の方針を立てました。これならなんとか出来そうです。
・テストメソッドの中でWindow.loopを回し、司エンジンを起動する。
・テストしたい処理を実行する。
・確認したいコントロールをfind_control()で取得する。
・そのコントロール以下の情報をserialize()でダンプする(PODの配列が出力される)
・ダンプした配列を、想定されるデータとassert_equalで比較する。
失敗/Test::Unit
それで試しに以下のコードを書いてみたら、ウィンドウが表示されずにテストが終了してしまいました。確認してみるとメインループは一回だけ実行されて、そのまま抜けてしまいます。
require 'test/unit' require 'dxruby' class TC_Foo < Test::Unit::TestCase def test_foo Window.loop() do #ここでテストを行う想定 end end end
成功/Minitest
mirichiさんに相談してみたところ(いつもすみません)。テストの自動実行がat_exitのタイミングで行われているが、DXRubyはそのタイミングでは既に処理が終了しているため、正常に動かないのだろうとのことでした。
それで、自動実行を抑制する方法を探したのですが、どうもTest::Unitにはそのオプションが(調べた限り)ないようで、試しにminitestの方で動かしたら意図した通りにウィンドウが表示されました(ただし自動実行を使うようにwarningが出ます)。
require 'minitest/unit' require 'dxruby' MiniTest::Unit.autorun class TC_Foo < Minitest::Unit::TestCase def test_foo Window.loop() do #ここでテストを行う想定 end end end
【追記】今度こそ本当に成功/Minitest
上記についてツイッターでmirchiさんに報告していたら、なんと、seattle.rb(アメリカのRubyコミュニティの一つ)に所属しているminitestの作者、Ryan Davisさんから「subclass 'minitest/test'」と突っ込みをいただいてしまいました(まじか)。
言われた通りに修正したらwarningが消えました。他の記述も微調整して、以下が最終形になります。よし、これでテストを書く準備ができたぞ! Thank you Ryan!!
require 'minitest/test' require 'dxruby' MiniTest.autorun class TC_Foo < Minitest::Test def test_foo Window.loop() do #ここでテストを行う想定 end end end
NEXT
・ひとまず動くことが確認できたので、今後少しずつテスティング環境を構築していきます。
・最終的にはDXRubyのモックを作り、I/O側のデータもテストできるようにするつもりです。
・もっと言えば、司エンジンとDXRubyの間にもう1層作り、環境に合わせて任意のゲームライブラリに差し替えできるようにしたいですねー。