システムの開発をしていれば、開発したものをテストすると思います。
単体テストや結合テストなど、テスト手法はいくつか存在しますが、想定していなかったケースに対してテストを行うことは難しいです。また最後はシステムテスト(実際に動かして、触ってみるテスト)を行うことになると思います。
(テスト手法などについてはこちらを参照)
テストを行うには相応の時間がかかり、テストに時間をかけられない場合もあります。また、開発者以外の人がテストを行うことも多くあります。
そして、いくらテストを行っても、バグを全て発見、修正することはできませんし、実際にゲームを公開し、運用していくことで見つかるバグも多くあります。(ユーザーは基本的に開発時には想定していなかったことも行なってきますので...)
実際の開発段階では見落とだったしがちな割と特殊なテストケースを(実際に自分がデバッグするときの備忘録として残すために)紹介します。
※ここに挙げる内容や主張は主観を多く含みます。プロジェクトやチームごとの方針があると思いますので、適宜、議論していただけたらと思います。またタイトルに「ゲーム」と書いていますが、多くのケースはゲーム以外のケースにも当てはまる話であるとも感じているのでそこら辺の部分は読み替えていただけたらと思います
通信系
近年、ゲーム内では通信が頻繁に行われるようになりました。
(通信のしくみなどについてはこちらを参照)
以降では通信についてよく見落とされるテストケースについて紹介していきます。
多重リクエスト(ボタン連打)
テストケース例
ガチャボタンを連打する
望まれる挙動
- ボタンを押した回数だけ、課金石を消費し、その回数分だけ抽選によって選ばれたものを取得できる。
または
- 1回だけ課金石を消費し、1回だけ抽選によって選ばれたものを取得できる。
解説
ゲームでよくされるチート手法とその対策 〜アプリケーションハッキング編〜 でも紹介しています。ボタンを連打するとコスト(課金石など)を払うことなく無限にアイテムが増殖したりしてしまうよなケースが多くあります。
ユーザーは無意識でやってしまうことが多いケースであるため、実際にゲームを運用していくとよく遭遇します。
より詳しい対策などについてはゲームでよくされるチート手法とその対策 〜アプリケーションハッキング編〜 を参照してください。
通信切断
テストケース例
- 通信中に通信ができない状態になる。
- 通信中に通信を一度切断し、切断後すぐに再接続する。
望まれる挙動
- 通信中に通信ができない状態になった場合、一定時間、再接続を試みる。その後、再接続できなかったら、通信ができなくなった旨をユーザーに知らせる。通信可能な状態となった後、通信を途中で切断した場所にて正常にプレイすることができる。
- 通信中に通信ができない状態になった場合、一定時間、再接続を試みる。再接続することができた場合、そのままプレイを続行することができる。
簡単に再現させる方法
1. 一時的に通信を切断するケース
通信中のスマートフォンをWifiから4G/LTEに切り替える。
2. 長期的に通信を切断するケース
3. 物理的に通信を切断するケース
1, 2の手法は端末によっては機能として搭載されていない端末も存在します。そのため、物理的に通信を遮断するケースも同様に紹介しました。金属の容器の中、つまり上の画像のようなスチール缶の中やアルミホイルで包むことで通信機器を物理的に電波を遮断することができます。
再現に適したシチュエーション
-
Websocketなどの双方向通信(リアルタイム通信)を行なっている場所で通信を切断する。(つまり常時通信を接続しっぱなしているような場所。ゲームでいうところのマルチプレイ中の場所)
解説
特にモバイルゲームにおいて、電車の中など、移動中にゲームをするといったケースも多くなりました。 移動中にゲームをしていると
- 通信中に一時的に通信が切断される
- トンネルの中など、一時的に電波が届かない所に行く
といったケースが頻繁に起こります。
通信が切断されてすぐに以下のような通信エラーを表示していると、なかなかゲームをプレイすることができません。このような場合、ユーザーとしてはストレスが溜まり、離脱や機会損失につながります。
このため、一時的に通信が切断されたり繋がらなくなったりしても、ある程度プレイを継続できるように再接続を試みたりといった対応を施しておくことが望ましいと思います。
また、ファイルのダウンロード中に通信を切断すると、ファイルが完全にダウンロードできていない状態となります。ユーザーが再度同じ場所にきた場合、そこでダウンロードしたファイルを使用しようとすれば当然エラーとなってしまいます。このため、完全にダウンロードできなかったファイルは削除し、再度最初からダウンロードするようにする、といった対策も施しておくことが望ましいと思います。
参考
非同期処理の画面への反映
テストケース例
通信中に別の画面に遷移して、ゲームを続ける。
望まれる挙動
- 通信中には別の画面に遷移できないようにする。
または
- 通信を送信した場所とは違う画面で通信結果を受け取っても、正常にゲームを続けることができる。
解説
非同期処理、特に通信は基本的に時間がかかる処理のため非同期処理で行わないと画面が固まってしまいます。非同期処理の結果を画面に反映させるような処理を行う場合、非同期処理を開始したときには想定していなかった画面の状態となっていることは多くあります。
例えば、以下のように通信が終われば通信中の画面から切り替わりそうですが同時に、通信中の画面であっても他の画面に移動することができそうです。
このように通信中に他の画面に移動し、移動したあとに遅れてエラーが発生してしまった、というケースもしばしば存在するため、このような場合において、画面を移動したら通信をキャンセルする(通信を受け取ってもその後の処理は実行しない)ように処理を施す必要があると思います。
望まれる挙動の解説
多くの場合、以下のように非同期処理中(通信中)の場合、その間、画面内のボタンを押せなくすることで、擬似的な同期処理にしています。
しかし、このように画面をロックするような通信待ちを頻発するとゲームのテンポが悪くなるため、ユーザーに快適にプレイしてもらうために画面をロックせず、非同期のままゲームを続けるような設計を用いる場合もあります。(参考例: 【CEDEC2017】人気タイトル『アナザーエデン』になぜ通信待ちストレスがないか。その理由は非同期オートセーブにあった)
プロジェクト、ゲームの方針によるとは思いますが、非同期のままゲームを継続するような設計にする場合、実装難易度が上がりますので注意が必要です。
通信障害
テストケース例
通信を行ったが、通信が返ってこなかったので、別の画面に行きプレイを続ける
望まれる挙動
最初に行われた通信はサーバーに届いた場合、正常に処理され、通信が返ってきた後でも正常にプレイを続けることができる。また、通信を行う順番を守る必要がある場合、サーバー側で受け取った処理の順番に即した処理が実行されること。
簡単に再現させる方法
サーバー上においてレスポンスを返すまでの時間をわざと遅らせる。
例えばrailsのcontrollerでは以下のような処理を書くことでレスポンスを返すまでの時間をわざと遅らせることができます。
# 5秒間返すリクエストを遅らせる
sleep 5
このようなsleep処理は他の言語でも利用することができると思うので言語に合わせて実装することをお勧めします。
解説
通信を使ったゲームの場合、常に快適な通信環境でユーザーに利用されるとは限りません。通信が届きにくい状況あるいはサーバーの負荷が大きいような状態で利用されることもあり、そのような場合ではリクエストを送ってもなかなか返ってきません。非同期処理の画面への反映の項目のように、通信がなかなか返ってこない中で画面をロックした場合はフリーズしたような状態となるので、ユーザーにストレスを与ることとなります。
そのため、非同期処理と併用していくことが実装する上で望ましいと思いますが、通信が返って来るまでの間にユーザーは様々なことを行いますし、意図していなかった順番で通信を受け取り、サーバー上で処理が行われるということもあります。
通信障害が発生している環境を実際に作るには、厳しい条件をつくらないといけないため、上記のように擬似的に通信障害が発生した環境を作り、その環境下でゲームをプレイすることでバグの発見に役立てられるのではないかと思います。
入力項目系
ユーザーにフリーワードで入力を求める場合、様々な内容のものが入力されます。また、多くの場合セキュリティリスクとしてもあげられます。
以降では入力項目系でよく見落とされるテストケースについて紹介していきます。
絵文字・特殊記号
テストケース例
入力項目に絵文字・特殊文字を含めて送信する
望まれる挙動
- 送られた絵文字・特殊文字が送信後の画面にも反映される。
または
- 一部、特定の絵文字・特殊文字は入力されても使用できないようにする
解説
絵文字や特殊文字は文字コードによる問題です。特にデータをMySQLなどに保存している場合、文字コードをutf8にしていても、絵文字付きのデータ保存しようとした場合、エラーとなったり、文字化けしたりします。そのため、utf8mb4するなど、用途に応じた文字コードを設定することで、対応する必要があります。
参考
SQLコマンド
テストケース例
入力項目にSQLを含めて送信する。
望まれる挙動
入力された文字が「文字列」としてそのまま表示・反映される
入力例
SELECT user_id,password FROM users WHERE user_id='1' or '1' = '1'
解説
いわゆるSQLインジェクションの対策をしているかチェックするために入力項目にSQLを記述して送信します。SQLインジェクションについては以下の画像の通りです。
SQLインジェクションは有名な攻撃手法で基本的にメジャーなサーバーフレームワークを使用していれば対策が施されています。そのため、メジャーなサーバーフレームワークを使って開発することで対策することを強くお勧めします
一応、自力で対策をする場合はエスケープ処理を施すか、placeholderを使うことで対策可能です。(詳細などは後日まとめます)
参考
OSコマンド
テストケース例
入力項目にOSコマンドを含めて送信する。
望まれる挙動
入力された文字が「文字列」としてそのまま表示・反映される
入力例
ls -l
なお、よく例示される例として、以下のようなものがあります。
rm -rf /
しかし、もし万が一対策が施されておらず実行されてしまった場合、悲惨な結末を迎えるため、上記コマンドを使っての確認はオススメしません。
解説
いわゆるOSインジェクションの対策をしているかチェックするために入力項目にOSコマンドを記述して送信します。上記、SQLインジェクションのOSコマンド版にあたります。
対策としてはそもそも入力項目の内容からOSコマンドを実行できるような処理を書かないようにするべきです。プログラムの中でOSコマンドの実行が必要な場合、入力項目の内容を用いないような仕組みにするべきであると思います。
参考
スクリプト
テストケース例
入力項目にjavascriptなどの動的に実行させることができるような文字列を含めて送信する。
望まれる挙動
入力された文字が「文字列」としてそのまま表示・反映される
入力例
<script>alert(document.cookie)</script>
解説
いわゆるクロスサイトスクリプティング(XSS)の対策をしているかチェックするために入力項目にJavascriptなどのスクリプトが実行できるようなものを記述して送信します。
クロスサイトスクリプティングは有名な攻撃手法で基本的にメジャーなサーバーフレームワークを使用していれば対策が施されています。そのため、メジャーなサーバーフレームワークを使って開発することで対策することを強くお勧めします
一応、自力で対策をする場合は<
は<
に、>
は>
に、&
は&
に、"
は"e;
に、'
は'
など、スクリプトとして認識されないけれども、表示されるものが同じになるように変換(エスケープ)処理を施すことで対策できます。
参考
バージョン互換
テストケース例
- 新しいバージョンのアプリがリリースされた中で古いバージョンのアプリでプレイする
- 古いバージョンのデータが端末内に残っている中で、新しいバージョンのアプリをインストールしてプレイする
望まれる挙動
- 古いバージョンのままプレイしようとしたらバージョンアップを促す表示を出し、古いバージョンではプレイできないようにする
または
-
古いバージョンのままプレイしても(古い機能などが使えるようなシチュエーションがないので)問題なくプレイすることができる。
-
古いバージョンにデータが残っていても新しいバージョンにデータが引き継がれてゲームをプレイすることができる
解説
アプリを更新する場合、プレイしているユーザー全員が最新のバージョンでアプリをプレイしてくれるわけではありません。古いバージョンには現在は使われなくなってしまった機能やイベントや仕様が残っているので、ユーザーにそれらが使われてプレイされる恐れがあります。
このような場合の対応として、基本的には以下のように古いバージョンのアプリではプレイできなくしてアップデートを促すのが一般的です。
しかし、アプリをアップデートするにはアプリをダウンロードしなければならないので、それまではユーザーにとってはプレイできるようになるまで待たなければなりません。そのため、過度にアップデートしなければならない状況を頻発するとユーザーの機会損失にも繋がります。そのため、バグ修正など、古いバージョンのままでもプレイしてもいいようなケースでは強制的にアップデートを促すようなことはしないものと併用することが望ましいと思います。
また、バージョンアップした場合であっても、端末内に保存されたデータやキャッシュはそのまま新しいバージョンでも利用されます。開発時にはこれらのデータを初期化して用いることが多いのですが、リリースした後にこれらのデータを初期化するような機会はあまりありません。そのため、古いデータが残ったままバージョンアップすると「なんかおかしい」というケースも度々起こるので、リリース前にこのようなバージョンアップの挙動について確認しておくべきだと思います。
正確な抽選確率
テストケース例
設定された確率通りの確率で抽選されたものが当たる。
望まれる挙動
抽選されて当たるものの確率がおおよそ設定された確率に近い分布になる。
簡単な再現方法
解説
(本当はあまり大きな声では言えないんですが)ガチャなどの抽選処理において、表記している確率で抽選を行なっているはずなのですが、実際の処理は出現確率通りになっていない場合がたびたびあります。原因として、
- 確率として設定した数字が間違えている。
- 抽選処理に問題があり、表示している確率通りに出現しないバグがある。
といったケースがあります。
この問題を放置し、発覚、拡散した場合、炎上の基となり、裁判などのきな臭い事態へと発展することもありますので十分に気をつけてください。
実際に表記してある確率通りに出るかどうか確認するためには、相応の回数を実行してみる必要があるのですが、実際に相応の回数を試行しないといけないためデバッグコストが高くなってしまいます。
そこで多少手間ではあるのですが、同じ抽選処理を使い、指定した回数だけ抽選を行い、その結果を表示してくれるようなシミュレーターを作成し、管理ツールなどに設置して、利用してもらうことで、デバッグコストを抑え、このような確率の設定ミスといったものを防ぐための手助けとすることができます。そのため、このようなシミュレーターを作成することを強くオススメします。
処理落ち
テストケース例
- ゲームを全体的にプレイする。
- ゲームを長時間起動してプレイする。
望まれる挙動
- 特にカクついたり、モッサリすることなくプレイすることができる
簡単な再現方法
解説
処理落ちとは、コンピュータ上において、一定間隔で行われるべき処理が、様々な要因によって本来想定・期待される間隔で行われずに、動作が止まったり遅延したりする現象を指します。ゲームにおいては例えば、「画面がカクカクする」とか「重い」とかいわれる現象です。
処理落ちの原因の多くは時間がかかる処理をメインスレッド(画面に表示する方のスレッド)で行っているためです。時間がかかる処理の原因となるものは多くありますが主な原因は以下の通りです。
など
また近年ではメモリ管理の関係上メモリが不足したら、スワップファイルからデータを読み込み始めたりするため、メモリ不足やメモリリークが原因で処理落ちし始めるといったケースもあります。
いずれの場合もいかなる場所でも処理落ちしないようにするには設計、実装することは労力がかかるため、マルチスレッドによる非同期処理や処理落ちしてもいい場所(ロード画面)を作ることで処理落ちしないようにコントロールするのが一般的です。
しかし、このような想定していてもメモリリークなどが原因で、処理落ちしてしまうケースは多くあるので、原因の調査のためにプロファイルーツールを使い、メモリの使用量やCPUの使用状況などを計測したり、ベンチマークを取り、処理時間を計測し、処理落ちするような箇所を特定し、対応する必要があります。
なお、これらの処理落ちの調査や修正はいくらプロファイラーを使い、ベンチマークをとっていったとしても原因の特定や修正には多大な労力がかかるため、実際にどれくらいやるかどうかは方針や計画を立てて対応することをオススメします
まとめ
以上がテストを行う場合によく見落とされるテストケースについて挙げていきました。(他にもよくあるケースはあるかもしれません。その場合は随時追加します。)
いずれも自動テストでは対策・確認するには難しいケースばかりだと思いますので、実際に動かしてテストしてみて、不具合やおかしいところがないか確認してみてください。(自動化できそうなケースであったのなら指摘していただけると幸いです)
開発者ではない方もテストをする際にこのようなイレギュラーなケースのテストを行なっていただき、バグやおかしな挙動の発見につなげていただけたらと思います。
また、確認や修正、実装には相応の時間がかかるため、場合によっては全て確認して対応することは難しいこともあると思います。そのため、ケースバイケースで対応することをオススメします。