IL2CPPの内部:テストフレームワーク

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 20, 2015|9 分
IL2CPPの内部:テストフレームワーク
このウェブページは、お客様の便宜のために機械翻訳されたものです。翻訳されたコンテンツの正確性や信頼性は保証いたしかねます。翻訳されたコンテンツの正確性について疑問をお持ちの場合は、ウェブページの公式な英語版をご覧ください。
これはIL2CPPの内部 シリーズの8番目で最後の投稿です。この投稿では、これまでの投稿の内容から少し逸脱して、IL2CPPがコンパイル時や実行時にどのように機能するかという点については触れないことにする。その代わりに、IL2CPPの開発とテストの方法を簡単に紹介しよう。
テストファーストの開発

IL2CPPチームは、テストファーストの開発メンタリティを強く持っている。IL2CPPのコードの多くは、テスト駆動開発TDD)を実践して書かれており、重要なテストカバレッジなしにIL2CPPのコードにマージされるプルリクエストはほとんどない。

IL2CPPには、ECMA 335仕様という有限の(かなり大きな)入力セットがあるため、その開発プロセスはTDDの概念にうまく適合する。ほとんどのテストは、プロダクション・コードの前に書かれる。そしてこれらのテストは、合格させるためのコードが書かれる前に、常に予想された方法で失敗する必要がある。

このプロセスは、IL2CPPの設計を推進するのに役立ちますが、開発チームには、IL2CPPの既存の動作のほとんどすべてを実行し、かなり迅速に実行されるテストの大規模なバンクを提供します。開発チームとして、このテスト・スイートは2つの重要な利点をもたらす。

1)自信:IL2CPPのコードをリファクタリングするほとんどの変更は、高い信頼性を持って行うことができる。テストがパスすれば、リグレッションが発生した可能性は非常に低い。

2) トラブルシューティングIL2CPPのコードは、私たちが期待するように動作するので、バグはほとんどの場合、コードの未実装の部分か、私たちがまだ考慮していないケースである。このようにバグの原因として考えられるものを絞り込むことで、バグをより迅速に修正することができる。

試験統計

IL2CPPのコードベースに対して実行するさまざまな種類のテストは、いくつかの異なるレベルに分かれる。各レベルのテストの数は以下の通りだ(各テストの種類については後述する)。

  • 単体テスト
  • C#:472
  • C++:44
  • 統合テスト
  • C#:1735
  • ILだ:173

これらのテストがすべてグリーンであれば、その時点でIL2CPPを出荷できると確信している。私たちはIL2CPPのために1つのメイン開発ブランチを維持し、それは常にUnity全体の開発の最先端ブランチを追跡しています。このメインの開発ブランチでは、テストは常にグリーンだ。壊れても(たまにあるけど)、たいてい数分以内に誰かが直してくれる。

私たちのチームの開発者は、個人的な開発のためにこのメインブランチを頻繁にフォークするので、常にグリーンである必要があります。メインの開発ブランチとパーソナルブランチの両方のビルドとテストのステータスは、Unityの内部ビルド管理システムであるKatanaで管理されています。

これらのテストを実行するためにNUnit を使用し、次の 3 つの方法のいずれかで NUnit を動かします。

  • Windows:ReSharper
  • OSX:Xamarin Studio
  • ビルドマシンのWindowsとOSXでのコマンドライン:カスタムPerlスクリプト

テストの種類

以上、あまり説明もせずに4種類のテストを紹介した。これらのテストはそれぞれ異なる目的を持ち、IL2CPPの開発を前進させるために協力し合う。

ユニットテストは、小さなコード(通常はメソッド)の動作を検証する。状況を設定し、テスト対象のコードを実行し、最後に期待される動作をアサーションする。

IL2CPPの統合テストは、実際にアセンブリ上でil2cpp.exeユーティリティを実行し、生成されたC++コードを実行ファイルにコンパイルし、実行ファイルを実行する。IL2CPPの動作(Unityで使用されているMonoの既存バージョン)に対する素晴らしいリファレンスがあるので、これらの統合テストはMono(およびWindows上の.Net)でも同じアセンブリを実行します。そしてテストランナーは、標準出力にダンプされた2つ(あるいは3つ)の実行結果を比較し、違いがあれば報告する。そのため、IL2CPPの統合テストでは、ユニットテストのようにテストコードに明示的な期待値やアサーションが記載されていない。

C#ユニットテスト

これらのテストは、私たちが書くテストの中で最も速く、最も低いレベルのものである。IL2CPP用のAOTコンパイラ・ユーティリティであるil2cpp.exeの多くの部分の動作を検証するために使用される。il2cpp.exeはすべてC#で書かれているので、高速なC#ユニットテストを使用することで、変更に要する時間を短縮できる。すべてのC#ユニットテストは、優れた開発マシン上で数秒で完了する。

C++ユニットテスト

IL2CPPのランタイムコード(libil2cppと呼ばれる)の大部分はC++で書かれている。公開APIから簡単にアクセスできないコードの部分については、C++のユニットテストを使用する。libil2cpp のコードのほとんどの動作は、より大規模な統合テストスイートで実施できるため、これらのテストは比較的少ない。これらのテストは、フィクスチャデータをセットアップするためにil2cpp.exeを実行する必要があるため、ユニットテストの実行よりも多くの時間を必要とします。

C#統合テスト

IL2CPPのための最大かつ最も包括的なテストスイートは、C#の統合テストスイートです。これらのテストは、icallの動作、コード生成、p/invoke、および一般的な動作を検証するテストに焦点を当て、より小さなセグメントに分割されている。このスイートに含まれるテストのほとんどは、5~10行程度の短いものだ。スイート全体はほとんどのマシンで1分以内に実行されるが、ストリッピングやコード生成などに関連するさまざまなIL2CPPオプションを使って実行することができる。

IL統合テスト

これらのテストは、C#の統合テストとツールチェーンが似ている。しかし、C#でテスト・コードを書く代わりに、ILGeneratorクラスを使って直接アセンブリを作成する。これらのテストは、C#のテストよりも書くのに少し時間がかかりますが、柔軟性が増します。現在使用しているMono C#コンパイラーでは、ILコードが無効であったり、生成されなかったりする問題にしばしば遭遇します。このような場合、ILコードで良いテストケースが書けることが多い。このテストは、conv.i(およびそのファミリーに属する類似のオペコード)のような、多くのわずかなバリエーションを持ちながら明確な動作をするオペコードの包括的なテストにも有益である。ILのテストはすべて1分以内に終了する。

これらのテストはすべて、カタナの多くのバリエーションとオプションを通して行った。ソースコードのクリーンなプルからテスト実行完了まで、ビルドファームの負荷にもよりますが、約20~30分のランタイムを要します。

なぜこれほど多くの統合テストを行うのか?

これらの説明から、IL2CPPのテスト・ピラミッドが逆さまになっているように見えるかもしれない。そして実際、エンドツーエンドの統合テスト(ピラミッドの頂点に近い部分)が、テストカバレッジのほとんどを占めている。

数秒以上のテスト時間でTDDの実践に従うことも難しい。私たちは、統合テスト・スイートの個々のセグメントを実行できるようにし、テスト・スイートで生成されたC++コードのインクリメンタル・ビルドを行うことで、これを軽減しています(この方法で、IL2CPPを使用したUnityプロジェクトのインクリメンタル・ビルドの可能性を実証しているので、期待してください)。それなら、個々のテストの納期は妥当なものだ(それでも我々が望むほど早くないが)。

統合テストを多用したのは、意識的な決断だった。IL2CPPのコードの多くは、2015年1月の最初の公開時でさえ、以前とは異なっている。IL2CPPのコードベースが始まって以来、私たちは多くのことを学び、多くの実装の詳細を変更してきたが、何年も前に書かれたオリジナルのテストの多くはまだ残っている。さまざまなレベルのテストを試した結果(生成されたC++ソースコードの内容の検証も含む)、これらの統合テストが、テストの安定性に対する実行時間の比率を最も良くしてくれると判断した。IL2CPPのコードに何か変更があったときに、既存の統合テストを修正する必要があることは、ほとんどない。この事実は、テストが失敗する原因となるコード変更が本当に問題なのかどうか、私たちに絶大な自信を与えてくれる。また、IL2CPPのコードのリファクタリングや改良を、必要なだけ、恐れることなく行うことができる。

さらに大規模なテスト

IL2CPP自体の外では、IL2CPPのコードは、より大きなUnityテストのエコシステムに適合している。IL2CPPをサポートして出荷する各プラットフォームについて、Unityプレーヤーのランタイムテストを実行します。これらのテストは、1000以上のシーンを持つ1つのUnityプロジェクトを構築し、各シーンを実行し、アサーションを介して期待される動作を検証します。通常、IL2CPPの変更のためにこのスイートに新しいテストを追加することはない(それらのテストは通常、より低いレベルで終わる)。このスイートは、あるプラットフォームでIL2CPPを使ったときに発生するかもしれないリグレッションに対するチェックの役割を果たす。また、このスイートによって、IL2CPPをUnityビルドツールチェーンに統合する際に使用するコードをテストすることができます。典型的なランタイム・テスト・スイートは約60~90分で完了するが、個々のテストをローカルで実行する場合はもっと速くなることが多い。

IL2CPPで使用するテストの中で最も大きく、最も遅いテストは、Unityエディタの統合テストです。これらのテストはそれぞれ、実際にはUnityエディタの異なるインスタンスを実行します。IL2CPPエディターの統合テストのほとんどは、プロジェクトのビルドと実行に重点を置いています。これらのテストは、複雑なエディターの統合、エラーメッセージの報告、プロジェクトのビルドサイズなどを検証するために使用します。プラットフォームにもよるが、統合テスト・スイートは数時間で実行され、頻繁ではないにしても、通常は少なくとも毎晩実行される。

これらのテストの影響は?

ユニティでは、「難しい問題を解決する」ことを指針のひとつにしています。私は問題の難しさを失敗という観点から考えるのが好きだ。解決するのが難しい問題であればあるほど、解決策を見つけるまでに多くの失敗をする必要がある。

Unityのスクリプトバックエンドとして使用する、高性能で移植性の高い新しいAOTコンパイラと仮想マシンを作成するのは難しい問題です。言うまでもなく、私たちはその過程で何千もの失敗を成し遂げてきた。解決しなければならない問題はまだある。しかし、そのような失敗のほとんどすべてから得られる有益な情報を、包括的で高速なテスト・スイートに取り込むことで、非常に迅速に反復することができる。

IL2CPPの開発者にとって、私たちのテストスイートは、バグがないコードを検証するための手段(バグを見つけることはできる)でも、IL2CPPを複数のプラットフォームに移植するための手段(それもできる)でもありません。

結論

IL2CPPの内部シリーズを楽しんでいただけたなら幸いです。可能な限り、実装の詳細を共有し、デバッグやパフォーマンスのヒントを提供させていただきます。IL2CPPの設計と実装に関連する他のトピックについてもっと聞きたい場合は、お知らせください。