IL2CPPで非同期メソッドのテストをする

最終更新日


Unityでasync/awaitのテストコードを書くのは手間がかかります。UnityのTestRunnerが非同期のテストに対応していないため、何らかの特殊な方法を取る必要があります。

さらにIL2CPPビルドして実機でテストしようとするとIL2CPPの制限も回避しなければならないために大変なことになります。そういった落とし穴と回避方法を紹介します。

非同期のテストを書く

まずはIL2CPPのことは忘れて非同期メソッドのテストを書きます。

例として以下のAddAsyncメソッドをテストすることにします。50ミリ秒待ってから足し算をするだけのメソッドです。

using System.Linq;
using System.Threading.Tasks;

public static class Calculator
{
    public static async Task<int> SumAsync(params int[] numbers)
    {
        await Task.Delay(50);
        return numbers.Sum();
    }
}

このメソッドをテストする方法を3つ紹介します。

1. Task.Run().Wait()を使う

一番簡単な方法はTask.Run()を使うことです。Task.Run()の中で普通にawaitを使ってテストを書き、Wait()で完了まで待ちます。

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

public sealed class AsyncMethodTest
{
    [TestCase(new[] { 0, 1, 2, 3 }, 6)]
    [TestCase(new[] { -10, 5, }, -5)]
    public void SumAsync_SimpleValues_Calculated(int[] numbers, int expected)
    {
        Task.Run(async () =>
        {
            int result = await Calculator.SumAsync(numbers);
            Assert.That(result, Is.EqualTo(expected));
        }).Wait();
    }
}

この方法のメリットは単純でわかりやすいことです。

デメリットは、テスト実行中はUnityエディタがフリーズすることです。Wait()でメインスレッドを止めているので当然です。

もしバグで無限ループが発生するといつまでもフリーズしたままなので強制終了するしかありません。テストがたくさんある中でフリーズすると、どのテストで止まっているのかすらわからず自動テストの意味がないです。

もう1つのデメリットとして、Wait()を書き忘れる可能性があることです。忘れるとどうなるかというと、テストコード中の全ての例外が無視されるようになります。つまりAssertに失敗しても検出されません。これまたテストの意味が無いです。表面上はテスト成功になって気付きづらいのでフリーズよりやっかいです。

結論としてこの方法は使ってはいけません。

2. コルーチンとしてテストする

TestRunnerでは非同期のテストが出来ませんがコルーチンのテストならできます。非同期メソッドをコルーチンとして実行することでテストします。

具体的には以下のようなコードになります。

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

public sealed class AsyncMethodTest
{
    [UnityTest]
    [TestCase(new[] { 0, 1, 2, 3 }, 6, ExpectedResult = null)]
    [TestCase(new[] { -10, 5, }, -5, ExpectedResult = null)]
    public IEnumerator SumAsync_SimpleValues_Calculated(int[] numbers, int expected)
    {
        Exception exception = null;

        Func<Task> method = async () =>
        {
            try
            {
                // 本当にテストしたい部分はここだけ
                int result = await Calculator.SumAsync(numbers);
                Assert.That(result, Is.EqualTo(expected));
            }
            catch (Exception e)
            {
                // テスト中に例外が発生したら保持しておく
                exception = e;
            }
        };
        Task tank = method.Invoke();

        // Taskが完了するまで待機
        while (!task.IsCompleted)
        {
            yield return null;
        }

        // 例外が発生していたら再スロー
        if (exception != null)
        {
            ExceptionDispatchInfo.Capture(exception).Throw();
        }
    }
}

taskを直接Waitする代わりにtask.IsCompletedを監視して完了するまで待っています。これなら毎フレームyield return null;でUnity側に処理を戻しているのでテスト中もフリーズしません。

このままだと直接テストに関係ない部分が多すぎて読みづらいので、テストを走らせるための部分は別メソッドに独立させます。

using System;
using System.Collections;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;

public static class AsyncTester
{
    public static IEnumerator Run(Func<Task> code)
    {
        Exception exception = null;

        Func<Task> method = async () =>
        {
            try
            {
                await code.Invoke();
            }
            catch (Exception e)
            {
                exception = e;
            }
        };
        Task task = method.Invoke();

        while (!task.IsCompleted)
        {
            yield return null;
        }

        if (exception != null)
        {
            ExceptionDispatchInfo.Capture(exception).Throw();
        }
    }
}

これを使ってテストコードは以下のように書けます。

using System.Collections;
using NUnit.Framework;

public sealed class AsyncMethodTest
{
    [UnityTest]
    [TestCase(new[] { 0, 1, 2, 3 }, 6, ExpectedResult = null)]
    [TestCase(new[] { -10, 5, }, -5, ExpectedResult = null)]
    public IEnumerator SumAsync_SimpleValues_Calculated(int[] numbers, int expected) => AsyncTester.Run(async () =>
    {
        int result = await Calculator.SumAsync(numbers);
        Assert.That(result, Is.EqualTo(expected));
    });
}

かなりわかりやすくなりました。

デメリットもないので基本的にこの方法で書くのが良いと思います。

3. UniTaskを使う

UniTaskを使っているなら同じように使えるメソッドが用意されているので自分で定義する必要はありません。

次のコードのようにUniTask.ToCoroutineを使います。

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

public sealed class AsyncMethodTest
{
    [UnityTest]
    [TestCase(new[] { 0, 1, 2, 3 }, 6, ExpectedResult = null)]
    [TestCase(new[] { -10, 5, }, -5, ExpectedResult = null)]
    public IEnumerator SumAsync_SimpleValues_Calculated(int[] numbers, int expected) => UniTask.ToCoroutine(async () =>
    {
        int result = await Calculator.SumAsync(numbers);
        Assert.That(result, Is.EqualTo(expected));
    });
}

UniTaskを使っている場合はこちらを使うのがいいと思います。

IL2CPPでビルドする

ここまでの説明でUnityEditor上でのテストはできるようになりました。ここからはIL2CPPビルドした状態でテストできるようにします。

テストをビルドするには、アセンブリの設定をPlayModeテストにしてTestRunner右上のRun All Testsをクリックすればできます。

しかし上記のAsyncMethodTestをビルドしようとすると以下のようなエラーが発生します。

Exception: IL2CPP error for type ‘System.ValueType’ in assembly ‘C:\UnityTest\Temp\StagingArea\Data\Managed\mscorlib.dll’ System.NotSupportedException: IL2CPP does not support attributes with object arguments that are array types.

IL2CPPでは属性の引数にobject型が使えないと言っています。エラー箇所が出力されず非常にわかりずらいのですが、TestCase属性で配列を渡している部分が引っかかっています。これをなんとかしないといけません。

TestCase属性が使えないならTestCaseSource属性を使ってみます。

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

public sealed class AsyncMethodTest
{
    public static IEnumerable SumAsync_SimpleValues_Calculated_Cases()
    {
        yield return new object[] { new[] { 0, 1, 2, 3 }, 6 };
        yield return new object[] { new[] { -10, 5, }, -5 };
    }


    [UnityTest]
    [TestCaseSource(nameof(SumAsync_SimpleValues_Calculated_Cases))]
    public IEnumerator SumAsync_SimpleValues_Calculated(int[] numbers, int expected) => UniTask.ToCoroutine(async () =>
    {
        int result = await Calculator.SumAsync(numbers);
        Assert.That(result, Is.EqualTo(expected));
    });
}

こうするとビルドしてテスト実行できるようになります。

しかしテスト結果が失敗になります。これはエディタ上で実行しても同じです。失敗メッセージは「Method has non-void return value, but no result is expected」です。戻り値を指定してあげないといけないようです。

TestCase属性のときは「ExpectedResult = null」で指定してました。TestCaseSource属性ではTestCaseDataを使って指定できます。

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

public sealed class AsyncMethodTest
{
    private static IEnumerable<TestCaseData> SumAsync_SimpleValues_Calculated_Cases
    {
        get
        {
            yield return new TestCaseData(new[] { 0, 1, 2, 3 }, 6).Returns(null);
            yield return new TestCaseData(new[] { -10, 5, }, -5).Returns(null);
        }
    }



    [UnityTest]
    [TestCaseSource(nameof(SumAsync_SimpleValues_Calculated_Cases))]
    public IEnumerator SumAsync_SimpleValues_Calculated(int[] numbers, int expected) => UniTask.ToCoroutine(async () =>
    {
        int result = await Calculator.SumAsync(numbers);
        Assert.That(result, Is.EqualTo(expected));
    });
}

これでビルドしてテスト実行できるテストコードになりました。


13件のフィードバック

  1. Hi there to every one, the contents present at this web page are genuinely awesome for people experience, well, keep up the good work fellows.

  2. Thank you a bunch for sharing this with all people you actually recognize what you are talking approximately!
    Bookmarked. Kindly also visit my site =). We could have a link change contract among
    us

  3. Today, I went to the beachfront with my kids. I found a sea shell and gave
    it to my 4 year old daughter and said “You can hear the ocean if you put this to your ear.” She placed the shell to her ear and screamed.

    There was a hermit crab inside and it pinched her ear.
    She never wants to go back! LoL I know this is
    totally off topic but I had to tell someone!

  4. Hey there! I just wanted to let you know that I’ve got some digital assets available
    for purchase on my Fiverr profile. You can head on over to my profile and check out my portfolio to see if there’s anything that catches your eye.

    Don’t hesitate to reach out if you have any questions or if you’re interested in placing an order.
    Thanks for stopping by! source link: fiverr.com/cpggarud2002

  5. Hey are using WordPress for your blog platform? I’m new
    to the blog world but I’m trying to get started and
    create my own. Do you need any html coding expertise to
    make your own blog? Any help would be greatly appreciated!

  6. Right here is the perfect webpage for anyone who wants to understand this topic.
    You know a whole lot its almost tough to argue with you (not that I actually will
    need to…HaHa). You certainly put a new spin on a subject that’s been written about for decades.
    Excellent stuff, just excellent!

  7. You’ve made some really good points there. I looked on the net for
    more info about the issue and found most people will go along with your views on this web site.

  8. I don’t even know how I ended up here, but I thought this post was
    great. I don’t know who you are but certainly you’re going to a famous blogger if
    you are not already 😉 Cheers!

  9. Wow, marvelous blog format! How long have you ever been blogging for?

    you made blogging look easy. The overall look of your
    site is fantastic, let alone the content material!

  10. I do believe all the ideas you have presented
    on your post. They are really convincing and can definitely work.
    Nonetheless, the posts are very short for novices.
    Could you please lengthen them a bit from subsequent time?

    Thanks for the post.

  11. Hmm is anyone else encountering problems with the images on this blog loading?
    I’m trying to figure out if its a problem on my end or if it’s
    the blog. Any feed-back would be greatly appreciated.

コメントを残す

メールアドレスが公開されることはありません。

コメントする