【Unity】Unity Test Runner(Test Framework)の小技色々まとめ

Unity Test Runnerの色々な小技をまとめました。

Unity 2019.3.5
Test Framework 1.1.11

複数の入力値をテストする

一つのメソッドで複数の入力値をテストするにはValueSourceアトリビュートを使用します。
引数には入力値を返すメソッドかフィールド、プロパティの名前を渡します。

using NUnit.Framework;

public class ExampleTest
{
    private static int[] _values = {1, 3, 9};
    
    [Test]
    public void Test([ValueSource(nameof(_values))]int value)
    {
        Assert.That(value * value, Is.LessThanOrEqualTo(10));
    }
}

上記の例では1と3と9という入力値についてそれぞれテストを行っています。
結果は以下のように表示されます。9のときだけ失敗していることがわかります。

f:id:halya_11:20200603230239p:plain
複数の入力値をテスト

ちなみにNUnitTestCaseアトリビュートも使えますが、コルーチン(IEnumerator)には使えないのでValueSourceを使っておいた方が無難です。

using NUnit.Framework;

public class ExampleTest
{
    [Test]
    [TestCase(1)]
    [TestCase(3)]
    [TestCase(9)]
    public void Test(int value)
    {
        Assert.That(value * value, Is.LessThanOrEqualTo(10));
    }
}

特定プラットフォームで実行したときだけテストする

特定のプラットフォームでのみ実行するテストを書くにはUnityPlatformアトリビュートを使います。

using UnityEngine;
using UnityEngine.TestTools;

public class ExampleTest
{
    [UnityTest]
    // macとWindowsのエディタ実行時にだけテストする
    [UnityPlatform(RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor)]
    public void Test()
    {
    }
}

ただし対象外のプラットフォームでも実行されないだけでコンパイルはされるので注意してください。

MonoBehaviourを使ったテストを書く

MonoBehaviourを使ったテストを書くには、IMonoBehaviourTestを実装したMonoBehaviourクラスを定義した上で下記のようにMonoBehaviourTestを使います。

using System.Collections;
using UnityEngine;
using UnityEngine.TestTools;

public class ExampleTest
{
    [UnityTest]
    public IEnumerator Test()
    {
        // MonoBehaviourTestを使う
        yield return new MonoBehaviourTest<TestBehaviour>();
    }
    
    public class TestBehaviour : MonoBehaviour, IMonoBehaviourTest // IMonoBehaviourを実装
    {
        // 一定の座標よりも下になったら終了
        public bool IsTestFinished => _target.position.y < -100;

        private Transform _target;

        // テスト対象のGameObject生成
        private void Awake() => _target = GameObject.CreatePrimitive(PrimitiveType.Sphere).transform;
        
        // 毎フレーム位置を下げていく
        private void Update() => _target.transform.position += Vector3.down;
    }
}

このテストではMonoBehaviour生成時にGameObjectを一つ生成し、
毎フレーム位置を下げていって一定の座標よりも下まで行ったら終了しています。

メモリアロケーションがあるかどうかテストする

メモリアロケーションがあるかどうかをテストするにはAssert.That()のConstraintに以下のようにIs.AllocatingGCMemory()を使います。

using NUnit.Framework;
using UnityEngine;
// Unityが拡張したNUnitのIsのメソッドを使うため明示的に定義しておく
using Is = UnityEngine.TestTools.Constraints.Is;

public class ExampleTest
{
    [Test]
    public void Test()
    {
        Assert.That(() =>
        {
            Debug.Log(Vector3.one);
        }, Is.AllocatingGCMemory());
    }
}

これはUnityがNUnitのIsクラスを拡張したものになるので、明示的にusing宣言をしておきます。

また実際にはメモリアロケーションがないことをテストすることが多いかと思います。
その場合にはIs.Not.AllocatingGCMemory()と書けばOKです。

Edit Modeテスト中にPlayモードに入れる

Edit Modeテストの途中でPlayモードに入れたり、Playモードを終了したりするには、
以下のようにEnterPlayModeExitPlayModeクラスを使います。

using System.Collections;
using UnityEngine.TestTools;

public class ExampleEditorTest
{
    [UnityTest]
    public IEnumerator Test()
    {
        // ここに書いたコードは非再生状態で実行される
        
        // PlayModeに入れる
        yield return new EnterPlayMode();
        
        // ここに書いたコードは再生状態で実行される
        
        // PlayModeを終了する
        yield return new ExitPlayMode();
        
        // ここに書いたコードは非再生状態で実行される
    }
}

スクリプトコンパイルしなおす

テスト用のスクリプトを動的に生成する場合など、スクリプトコンパイルしなおすにはRecompileScriptsクラスを使います。

using System.Collections;
using System.IO;
using UnityEditor;
using UnityEngine.TestTools;

public class ExampleEditorTest
{
    [UnityTest]
    public IEnumerator Test()
    {
        // 適当にスクリプトファイルを生成
        File.WriteAllText("Assets/TestClass.cs", "public class TestClass{}");
        
        // AssetDatabaseに認識させる
        AssetDatabase.Refresh();
        
        // コンパイルを待つ
        yield return new RecompileScripts();
    }
}

また、dllを動的に追加したときなどにUnityの処理を待機するにはWaitForDomainReloadクラスを同じようにして使います。

テスト開始・終了時のコールバックを得る

テストの開始や終了のコールバックを得るにはTestRunCallbackアトリビュートを使います。

using NUnit.Framework.Interfaces;
using UnityEngine;
using UnityEngine.TestRunner;

[assembly:TestRunCallback(typeof(TestCallback))]
public class TestCallback : ITestRunCallback
{
    public void RunStarted(ITest testsToRun)
    {
        Debug.Log("テスト開始");
    }

    public void RunFinished(ITestResult testResults)
    {
        Debug.Log("テスト終了");
    }

    public void TestStarted(ITest test)
    {
        if (!test.IsSuite)
        {
            Debug.Log($"個々のテスト開始 : {test.Name}");
        }
    }

    public void TestFinished(ITestResult result)
    {
        if (!result.Test.IsSuite)
        {
            Debug.Log($"個々のテスト終了 : {result.Test.Name} / {result.ResultState.Status}");
        }
    }
}

テスト用のPrimitiveのGameObjectを作る

UnityEngine.TestTools.Utils.Utils.CreatePrimitive()を使うとテスト用のプリミティブのGameObjectを作成できます。
挙動としては、GameObject.CreatePrimitive()と同様ですが、テスト用に軽いシェーダがアサインされます。
よってGameObject.CreatePrimitive()よりもテスト時のパフォーマンスがよくなります。

関連

light11.hatenadiary.com

参考

docs.unity3d.com