【Unity】Unity Test Runner(Test Framework)で例外やエラーログが出力されることをテストする方法まとめ

Unity Test Runnerで例外やエラーログが出力されることをテストする方法をまとめました。

Unity 2019.3.5
Test Framework 1.1.11

はじめに

UnityのTest Runnerでは、テストメソッド内で例外が発生したりDebug.LogErrorが実行されるとそのテストは失敗したものとしてみなされます。
しかしこれでは「例外が正しく発生すること」を確認するテストや「正しくエラーログが出力されること」を確認するテストを書きたいときに困ります。

本記事ではこのように例外やエラーログの出力をテストする方法をまとめます。

なお、Unity Test Frameworkの基本的な使い方は以下の記事で紹介していますので、必要に応じて参照してください。

light11.hatenadiary.com

例外をテストする

それではまず例外をテストする方法をまとめます。

例外が出力されることをテストする

例外が狙い通りに出力されることを確認するためにはNUnitAssert.Throw()を使用します。

using System;
using NUnit.Framework;

namespace ExampleTests
{
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {
            // 指定した型の例外がテスト対象コードで吐かれたらテスト成功となる
            Assert.Throws<InvalidOperationException>(() =>
            {
                // ここにテスト対象のコードを記述する
                // 今回は対象のコードでInvalidOperationExceptionがスローされたと仮定する
                throw new InvalidOperationException();
            });
        }
    }
}

上記のように書くと、テスト対象のコード内でInvalidOperationExceptionがスローされたらテスト成功となります。
例外がスローされなかったり、他の例外がスローされたらテスト失敗です。

例外のメッセージをテストする

また例外のメッセージなどを検証したい場合には以下のようにAssert.Throws()を使用します。

using System;
using NUnit.Framework;

namespace ExampleTests
{
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {
            var ex = Assert.Throws<InvalidOperationException>(() => throw new InvalidOperationException("test"));
            Assert.That(ex.Message, Is.EqualTo("test"));
        }
    }
}
例外の派生クラスも許容するAssert.Catch()

さて前節のAssert.Throws()は指定した例外の型とスローされる型が完全に一致する場合のみテスト成功となります。
もし派生した型も許容したい場合には代わりにAssert.Catch()を使用します。

using System;
using NUnit.Framework;

namespace ExampleTests
{
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {
            // これはテスト成功となる
            Assert.Catch<Exception>(() =>
            {
                throw new InvalidOperationException();
            });
            
            /*
            // cf. これはテスト失敗となる
            Assert.Throws<Exception>(() =>
            {
                throw new InvalidOperationException();
            });
            */
        }
    }
}
例外がスローされないことをテストする

なお単純に例外がスローされないことをテストするにはAssert.DoesNotThrow()が使えます。

using NUnit.Framework;

namespace ExampleTests
{
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {
            Assert.DoesNotThrow(() =>
            {
                // ここにテスト対象のコードを書く
            });
        }
    }
}

Unityのログ出力をテストする

それでは次にUnityのログに関連するテストの書き方をまとめます。

エラー出力されることをテストする

まずDebug.LogError()によりエラーログが出力されることを確認するにはLogAssert.Expect()を使用します。

using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace ExampleTests
{
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {
            // 指定したメッセージのLogErrorが出力されることをテストする
            LogAssert.Expect(LogType.Error, "test error.");

            Debug.LogError("test error.");            
        }
    }
}

指定したメッセージのエラーログが出力されるとテストが成功になります。

また判定はフレームの最後に行われるため、コルーチンを使ったテストも考えると
テストコードよりもLogAssert.Expect()を先に書いておく必要があります。
例えば以下のテストは失敗します。

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

namespace ExampleTests
{
    public class ExampleTest
    {
        [UnityTest]
        public IEnumerator ExampleTest01()
        {
            Debug.LogError("test error.");
            yield return null;
            
            // テスト対象のコードより前に書いておかないと判定されない
            // このテストは失敗します
            LogAssert.Expect(LogType.Error, "test error.");
        }
    }
}
他のログ出力をテストする

ちなみにLogAssert.Expect()の第一引数のLogTypeには以下の種類も用意されています。

  • LogType.Error
  • LogType.Assert
  • LogType.Warning
  • LogType.Log
  • LogType.Exception

したがって、例えば「特定の警告が出力されること」をテストしたりもできます。

正規表現でメッセージをテストする

またLogAssert.Expect()の第二引数のメッセージには正規表現を指定することもできます

using System.Text.RegularExpressions;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace ExampleTests
{
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {            
            LogAssert.Expect(LogType.Error, new Regex("^test"));

            Debug.LogError("test error.");
        }
    }
}
すべてのログ出力がExpectされているかテストする

LogAssert.NoUnexpectedReceived()を使うと、すべてのログ出力がLogAssert.Expect()の対象となっているかをチェックできます。

using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace ExampleTests
{
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {
            LogAssert.Expect(LogType.Error, "test error.");
            
            Debug.LogError("test error.");
            Debug.Log("test.");
            
            // この時点でLogAssert.Expectの対象になっていないログ出力が行われていたら失敗
            // この例ではDebug.Log("test")が対象になっていないのでテストは失敗する
            LogAssert.NoUnexpectedReceived();
        }
    }
}

このメソッドは呼び出した時点でチェックを行うようなので、
上の例のようにテストコードの最後に書く必要がある点に注意が必要です。

また、クラス内のすべてのメソッドにこれを適用したい場合には、
クラスにTestMustExpectAllLogsアトリビュートを付ければ同等の結果が得られます。

using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace ExampleTests
{
    // 全てのログ出力がLogAssert.Expectの対象になっていないとテストを失敗にする
    [TestMustExpectAllLogs]
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {
            LogAssert.Expect(LogType.Error, "test error.");
            
            Debug.LogError("test error.");
            Debug.Log("test."); // これのせいでこのテストは失敗する
        }
    }
}
ログ出力によるテストの失敗をさせないようにする

そもそもエラー出力が行われてもテストを失敗扱いにしないようにするにはLogAssert.ignoreFailingMessagesをtrueにします。

using System;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace ExampleTests
{
    public class ExampleTest
    {
        [Test]
        public void ExampleTest01()
        {
            LogAssert.ignoreFailingMessages = true;
            
            // テストは失敗しない
            Debug.LogError("test error.");
            Debug.LogAssertion("test assertion.");
            Debug.LogException(new Exception());
        }
    }
}

なおこのフラグは各テストの終了後にデフォルト値(false)に戻るので各テストの先頭で設定する必要があります。

関連

light11.hatenadiary.com

参考

nunit.org

docs.unity3d.com