【Unity】Unity Test Frameworkで非同期メソッドをテストする

Unity Test Frameworkで非同期メソッドをテストする方法をまとめました。

Unity2019.3.3
Unity Test Framework 1.1.14

Unity Test Framework?

Unity Test Framework(Unity Test Runner)はUnityで単体テストを行うためのライブラリです。
内部的にはNUnitが使用されています。

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

light11.hatenadiary.com

Unity Test Frameworkは非同期メソッドに対応していない

さて、まずはそもそもUnity Test Frameworkが非同期メソッドに対応していないよねという話からです。
試しにUnityで以下のようなテストコードを書いてみます。

using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

public class Example
{
    [Test]
    public async Task Test()
    {
        await Task.Run(() => Thread.Sleep(1000));
        Assert.That(true);
    }
}

見ての通りテストの内容は全く無意味ですが、正常に非同期メソッドのテストができるのであれば、
テストを実行した1秒後にそのテストが成功するはずです。

しかし実際にはテストを実行するまでもなくエラーになります。

f:id:halya_11:20200517235610p:plain
テストを実行するまでもなくエラー

エラー内容としては「Method has non-void return value, but no result is expected」と表示されていることがわかります。
どうやらUnityが使っているNUnitが古い関係で非同期メソッドのテストができないようです。

いずれ修正されるかもしれませんが、現時点では非同期メソッドのテストには何かしら工夫が必要そうです。

同期的に実行する

じゃあ非同期メソッドじゃなくせばいいやということで、以下のように同期的に実行してみます。

using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

public class Example
{
    [Test]
    public void Test()
    {
        Task.Run(() => Thread.Sleep(1000)).GetAwaiter().GetResult();
        Assert.That(true);
    }
}

これはテストは通るのですが、UIスレッドがブロックされるのでテスト中にUnityが操作不可能になります。
またデッドロックが発生するとUnityが固まったままになります。キャンセルも不可能です。

デッドロックを防ぐ

この問題を解決するために、Unity Test Frameworkでコルーチンをテストできる特性を利用します。
Taskの終了待ちを以下のようにコルーチンで処理するように書き換えます。

using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using UnityEngine.TestTools;

public class Example
{
    [UnityTest]
    public IEnumerator Test()
    {
        var task = Task.Run(() => Thread.Sleep(1000));
        yield return task.AsIEnumerator();
        Assert.That(true);
    }
}

public static class TestExtensions
{
    public static IEnumerator AsIEnumerator(this Task task)
    {
        while (!task.IsCompleted)
        {
            yield return null;
        }
     
        if (task.IsFaulted)
        {
            throw task.Exception;
        }
    }
}

コルーチンをテストするためにはアトリビュート[UnityTest]する必要があるので注意してください。
これでメインスレッドがブロックされ続けることはなくなり、テストのキャンセルもできるようになります。

ちなみに結果を返すTaskのテストは以下のように行います。

[UnityTest]
public IEnumerator Test()
{
    var task = Task.Run(() =>
    {
        Thread.Sleep(1000);
        return 1;
    });
    yield return task.AsIEnumerator();
    
    Assert.That(1 == task.Result);
}

参考

https://forum.unity.com/threads/async-await-in-unittests.513857/