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

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

rubyゲーム開発にユニットテスト/テスティングフレームワークを導入する【説明編】

 Ruby Game Developing Advent Calendar 2016(http://www.adventar.org/calendars/1647) の9日目の記事です。8日目はうのはな透さんの「Enumerableを信じよ」でした。信じます!

 今回のRGDAC2016(あんまり略せてる気がしない)、どの記事も読み応え抜群なのですが(逆にrubyゲームプログラミング初心者がついてこれるのか心配)、特にしのかろさんのtapに感銘を受けました。なにこれすごい便利(きゅん)。

 9日目は「ゲーム開発にテスティングフレームワークを導入してみるよ」という話です。ruby+DXRuby+司エンジンでの事例ですが、概念はゲーム開発全般に応用が効くのではと思います(希望)。

 書いていたらテスティングフレームワークの説明だけでやたら長くなってしまったので二回に分けたいと思います。決して記事を水増ししてACの枠を埋めたいと思っているわけではありません。ええ、もちろんですとも!

あるゲーム会社での例

 ゲーム開発の現場を例にして「テストがないと何が起きるのか」を考えてみましょう。

 プログラマAさんがダメージ計算式のプログラムを作っているとします。このプログラムに必要なパラメータを入れると、適切なダメージ値が返ってきます。

 ある日、プログラマBさんが、このダメージ計算式が起動した時、そのダメージ値を画面に表示するプログラムを追加します。ダメージ値なのでマイナスの値を前提としておらず、仮にマイナス値が入ったら何が起きるかわからないコードでしたが、手元で動かしたら問題なかったのでコミットして退社します。Bさんは明日は有給申請済みです。

 さて、Bさんは知らなかったのですが、実はダメージ計算式は、回復魔法を使った時にこのプログラムがマイナス値を出力することでHPを回復するロジックが入っていました。

 次の日、実機テストしていると、バトル時にハングする現象が多発します。どうやら回復魔法を使うと落ちるようなのですが、誰にも原因がわかりません。ダメージ計算式は問題なく機能しているのに! 結局、その日1日はバグが取れず作業が滞り、プログラマさんの工数が1日分溶けたのでした。

 上記は極端な例ですが、規模の大小はあれど、開発中のあらゆる場面でこのような事が起こりうる(そして実際起きている)のがご理解頂けるかと思います。

ユニットテストとはなにか

 このように「コードの一部を修正した結果、別の場所で不具合が発生した」というのは、プログラミングにおいて頻発するシチュエーションです(ですよね!?)。複数人で開発する場合はさらに深刻な問題となりえます。

 これを回避するのに有効な手段は「愚直にテストコードを沢山書く」という事です。様々な条件をチェックするテストコードを作り、コードを改修する度にそれらを全て実行し、デグレの発生を検知するわけです。

 この場合、メソッドを単位に動作をチェックするテストが主になり、これをユニットテストと呼びます(Unit test. 日本語では単体テストとも)。もし1つでもエラーが出たらデグレが起きたと分かるので、すべてのテストが通るまで修正します。

 機能が増えたり、既存のテストだけではチェックしきれない事象が判明したら、その度にテストコードを追加します。これによってテストがカバーできる範囲(「カバレッジ」と言います)が広がり、(理屈の上では)潜在バグのないコードリポジトリが維持できるわけです。

テスティングフレームワークとはなにか

 テスティングフレームワーク(あるいはテスト駆動開発)とは、乱暴に言えば上記の「言ってることは分かるけどそんな面倒なことやってらんないよ」という作業を、できるだけ負荷をかけずに実現する手法の総称です。ケント・ベックがエクストリームプログラミングの要素の1つとして紹介したのをきっかけに世界中に広まり、現在、ビジネスアプリやOSSの開発ではテストコードを添付することがほぼ前提になっています。

 標準的なテスティングフレームワークでは、テスト用のクラスを継承し、テスト1つに対応するメソッドを記述します。テストメソッドの中で、テストしたいメソッドを呼び出し、(多くの場合)返し値をassert_xxxxxという各種判定用のメソッドで確認し、メソッドが期待される挙動を取っているかを判定します。

 テストコードを書いたら、あとはフレームワークが自動的にクラスを生成し、実装されたメソッドを順に全て実行し、テストの通過状況をログとして出力します。テストクラスを生成したり、テストメソッドを呼び出すコードを書かなくてもフレームワークが代わりに実行してくれるのです(これを実現するために、テスティングフレームワークではメタプログラミングやマクロ展開や静的解析によるコード生成が駆使されます)。このような統一的/半自動的な手法を導入することで、ユニットテストをしやすくするわけです。

土屋がゲーム開発でのユニットテストに否定的だった理由

 土屋は以前から、ゲームプログラミングにテスティングフレームワークを導入する方法を検討していたのですが、なかなか上手い方法が思いつかず、恐らく困難だろうと考えていました(以前の記事でも書きました>http://d.hatena.ne.jp/t_tutiya/20161127/1480241989)。個人的に課題は大きく二つあると考えていました。

 一つ目の課題は返し値です。ユニットテストではメソッドを実行してその返し値が想定と同じかどうかを判定するのが一般的かと思うのですが、ゲームプログラムでは、メソッドの実行結果はほとんどの場合に画面やスピーカーなどのI/Oへ出力にされるので、返し値には大した情報が含まれておらず、テストの値に使うには適さないのです。

 もう一つの課題はテストコードの書き方です。ゲームプログラミングは秒間60回実行されるメインループを中心に実行されるのですが、「そもそも「あるテストメソッド内で、複数フレームに渡る処理を経た後の状態を判定する」というのはどういうコードなのか?」という問いに上手い答えを出せず、テストの導入は延々と先延ばしにされていたのでした。

 そんな中、11月に友達のビジネスアプリのスーパーエンジニアの太一(https://twitter.com/ryushi)さんと飲む機会があって、この話をした所盛り上がり、最終的に司エンジンのコードを一部読んでもらい、どういうテストコードを書けば良いのかのアドバイスを沢山もらいました。ありがとう太一さん。

 この時に感銘を受けた言葉が「テストコードをどう書くかも大事だが、テストコードが書きやすくなるように実装の方を修正することも大事」という物でした。なるほど、確かにそれはそうだ!
 それで、v2.1ではテストを書きやすくなるようにコードを大きく修正しています。一番大きいのは、これまで固定だったコントロールツリーのルートオブジェクトを任意に設定できるようになったことで、これによって柔軟なテストの実行が期待できます。

 本日はここまで。次回は実際に動作するテストコードを書いていきます。