前回の Unity で ユニットテストをする では、 Editor Test Runner を使って、MonoBehaviour をテストしてみた。

ただし、Coroutine や非同期な API のテストには対応していない。 これらのテストは、Unity Test Tools の Integration Test Frameworks を使って実行することができる。

ということで今回は、Unity Test Tools を使って WWW をテストしてみる。

概要

  • Unity Test Tools を使って WWW のユニットテストを実行してみる
  • Unity Editor の GUI からテストを実行してみる
  • Jenkins でテストを実行し、テスト結果を表示してみる

環境

  • Unity 5.3.2

基本的なこと

Unity-Technologies / UnityTestTools / wiki / IntegrationTestsRunner — Bitbucket を読む。

Unity Editor で実行する

まずは、GUI(Unity Editor)から実行してみる。

アセットを import する

Asset Store から "Unity Test Tools" を import する。

ユニットテストを定義する

Integration Test Runner は、テスト用のシーン内でテストを実行する。 テスト用のシーンに被テストオブジェクトを配置することで、オブジェクトの結合テストができる。

テスト用のシーンを作成する

  1. 空のシーンを作成する
    • WWW クラスのテストをしたいので NetworkingTests という名前で作成した
  2. TestRunner を配置する
    • 空の GameObject を作って、TestRunner コンポーネントを付ければ OK

テストコードを書く

今回は、ユニットテストをしたいだけなので、わざわざ手動でシーンにオブジェクトを配置するのは面倒くさい。 Integration Test Framework では、テストコードにシーン名を記述しておくと、 テスト実行時に自動でオブジェクトを配置してくれる機能があるので、それを使う。

詳しくは、Wiki より "Creating Integration Tests from code (Dynamic Integration Tests)" の項を読む。

Tests/IntegrationTests/NetworkingTests/WWW_Get_ReceivesContent.cs

using UnityEngine;
using UnityEngine.Assertions;
using System.Collections;

[IntegrationTest.DynamicTest("NetworkingTests")]
[IntegrationTest.Timeout(10)]
public class WWW_Get_ReceivesContent : MonoBehaviour
{
    IEnumerator Start()
    {
        var sut = new WWW("http://google.com");
        yield return sut;

        // そんな Assertion で大丈夫か?
        Assert.IsTrue(sut.text.Contains("<title>Google</title>"));
        IntegrationTest.Pass();
    }
}

Tests/IntegrationTests/NetworkingTests/WWW_Get_ReceivesError_WhenPageIsNotFound.cs

using UnityEngine;
using UnityEngine.Assertions;
using System.Collections;

[IntegrationTest.DynamicTest("NetworkingTests")]
[IntegrationTest.Timeout(10)]
public class WWW_Get_ReceivesError_WhenPageIsNotFound : MonoBehaviour
{
    IEnumerator Start()
    {
        var sut = new WWW("http://google.domainnotfound");
        yield return sut;

        Assert.AreEqual("Couldn't resolve host 'google.domainnotfound'", sut.error);
        IntegrationTest.Pass();
    }
}

配置する場所

Editor Test Runner で実行するテストコードは、Editor フォルダ内に配置することができたが、 Integration Test Runner の場合は、Editor に入れることができないっぽい(調査中)。 Editor に入れた場合、テストコードを MonoBehaviour のサブクラスにしてもシーンに配置できなかった。

ファイル名の付け方

決まりはないので自由に付けて良い。今回は、以下のような決まりで付けてみた。

<被テストクラス名>_<被テストメソッド名>_<振る舞い(動詞 + 目的語など)>_<条件(When/If など)>

ファイル 1 個がテストケース 1 件と対応するので、NUnit におけるテストメソッド名のようなノリで書いたら分かりやすい。ただし、ファイルを分けるのは、テストメソッドを分けるのと比較してだいぶ面倒くさいので、そこはバランスを意識する必要がある。

実行する

Unity Test Tools > Integration Test Runner でテスト一覧が開く。

unity integration test runner

ウィンドウ左上の、Run XXX からテストを実行できる。 現在開いているシーンに対応したテスト一覧が表示されるので、他のシーンのテストを実行したかったら、そのシーンを開く必要がある。

Jenkins で実行する

せっかくユニットテストをするので、CI でテストを実行したい。 Test Runner が出力してくれるテスト結果も表示する。なお、カバレッジは取らない。

と前回書いたとおり、今回もそうする。

CLI から実行する

-executeMethod オプションで Integration Test Framework のメソッドを指定すると、CLI からユニットテストを実行できる。

より詳しくは、Wiki より "Headless running (batch mode)" の項を参照する。

シェルの実行

Jenkins ジョブの Build > Execute shell でユニットテストを実行する。 ただし、ジョブに直接スクリプトを書き込むのではなく、スクリプトはファイルにして、ジョブではファイルを実行するだけにすると、 開発環境のローカルマシンでもテストを実行できたり、スクリプトを Git 管理できたりして捗る。

と前回書いたとおり、今回もそうする。

test_integration.sh

if [ -z "${WORKSPACE}" ]; then WORKSPACE=$(PWD); fi

/Applications/Unity/Unity.app/Contents/MacOS/Unity -batchmode -projectPath ${WORKSPACE} -executeMethod TestHelper.SetUp -quit
/Applications/Unity/Unity.app/Contents/MacOS/Unity -batchmode -projectPath ${WORKSPACE} -executeMethod UnityTest.Batch.RunIntegrationTests -resultsFileDirectory=${WORKSPACE}/EditorTestResults

EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
  cat $HOME/Library/Logs/Unity/Editor.log
  exit $EXIT_CODE
fi

ファイルは、プロジェクトルートに配置することを想定している。また、OSX で実行することを想定している。

4 行目でテストを実行する前に、3 行目で TestHelper.SetUp を起動しているのは、Unity PRO じゃない場合のワークアラウンドで、以下のスクリプトを実行している。

テストを実行した後に、結果を出力するところでタイムアウトしていたので、Test Runner の実装を読んでみたところ、 ビルドターゲットが iOS や Android になっていると、出力先に接続できずに落ちていることが分かった。 これを回避するために、テストを実行する前に、ビルドターゲットを OSX にしている。

Integration Test Runner は、ビルドターゲットでテストを実行してくれるので、 PRO ライセンスがあるなら、本番環境と同じビルドターゲットを使うのが良い。

Editor/TestHelper.cs

using UnityEngine;
using UnityEditor;

public class TestHelper : MonoBehaviour
{
    public static void SetUp()
    {
        //
        // Reporting integration test results requires
        //
        //   UNITY_PRO_LICENSE || !(UNITY_ANDROID || UNITY_IPHONE)
        //
        // See "UnityTestTools/IntegrationTestsFramework/TestRunner/TestRunnerConfigurator.cs"
        // See https://bitbucket.org/Unity-Technologies/unitytesttools/src/a30d562427e9fec0201e7a1e041aeb6659cbec93/Assets/UnityTestTools/IntegrationTestsFramework/TestRunner/TestRunnerConfigurator.cs?at=stable&fileviewer=file-view-default
        //
        const BuildTarget target = BuildTarget.StandaloneOSXUniversal;

        EditorUserBuildSettings.SwitchActiveBuildTarget(target);
    }
}

テスト結果を表示する

Integration Test Runner が出力するテスト結果を NUnit Plugin - Jenkins - Jenkins Wiki で表示する。

Post-build Actions > Publish NUnit test result report で EditorTestResults/*.xml を読ませれば良い。

jenkins job runs unity integration test

まとめ・感想

  • Unity Test Tools の Integration Test Runner を使って WWW のユニットテストを実行した
  • ユニットテストを CLI から実行する手順をスクリプトにまとめた
  • Personal ライセンスでテスト結果が表示されない問題のワークアラウンドを用意した
  • Jenkins でユニットテストを実行し、テスト結果を表示した

Editor Test Runner を使う場合と比べて、だいぶ面倒くさい。 特に、テストケース毎にファイルを分けると、テストコードの重複が増えてしまうのが、テストコードのメンテナンスコストを上げてしまう。

また、UnityEngine の Assertion が NUnit のものと比べて、イケてない。Assert.AreEqualAssert.That Is よりも読みづらい。

Editor Test Runner でも async が使えるようになる日が来ると嬉しい。

参考