8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

UnityAdvent Calendar 2021

Day 14

UnityTestRunnerでUniTaskを使おう!

Last updated at Posted at 2021-12-13

本記事は Unity Advent Calendar 2021 14日目の記事です。

はじめに

UnityTestRunner というものを知っていますか?
Unity で簡単に単体テストや結合テストができる機能です。
シーン再生によるオーバーヘッドを感じることなく処理を実行できるので、テスト以外にも結果をぱぱっと確認したいときとかにも便利です。

本記事では UnityTestRunner のチュートリアル記事ではないので詳しい説明は省きます

環境

Unity 2020.3.24f1
Test Framework 1.1.29
UniTask 2.2.5

UnityTestRunnerで非同期テスト

UnityTestRunner で非同期処理のテストをしたいときは、メソッドに UnityTestアトリビュート をつけ、返り値は IEnumerator にします。

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

public class EditModeTest
{
    [UnityTest]
    public IEnumerator EnumeratorTest()
    {
        yield return null;
        Assert.That(0,Is.EqualTo(0));
    }
    
    [UnityTest]
    public IEnumerator EnumeratorTest2()
    {
        yield return new WaitForSeconds(1);
        Assert.That(0,Is.EqualTo(0));
    }
}

ここで注意なのは EditMode だと EnumeratorTest2 はテストで必ず失敗するということです。
EditMode では、非同期を待つ処理は yield return null; など限られたもののみテストが可能です。

screenshot.2021-12-08.png

柔軟な非同期処理をテストしたい場合は PlayMode でテストするしかありませんでした。

UniTask で非同期テストしよう!

UnityTestRunner で UniTask を使うときはテストフォルダの AssemblyDefinition に UniTask への参照を追加しましょう。

screenshot_1638902743.png

UniTask を使えるからと言って、テストメソッドの返り値を async UniTaskasync UniTaskVoid にすることはできません。
が、UniTask には UniTask.ToCoroutine というメソッドが用意されています。

UniTask.ToCoroutine

[UnityTest]
public IEnumerator UniTaskTest() => UniTask.ToCoroutine(async () => {
    var cts = new CancellationTokenSource();
    // 1000㍉秒待つ
    await UniTask.Delay(1000, cancellationToken : cts.Token);

    // FixedUpdateのタイミングで60フレーム待つ
    await UniTask.DelayFrame(60,PlayerLoopTiming.FixedUpdate, cts.Token);

    // UnityWebRequestでHTTP通信をする
    var result = await UnityWebRequest.Get("https://Qiita.com").SendWebRequest().ToUniTask(cancellationToken : cts.Token);
    Assert.That(result.downloadHandler.text,!Is.Empty);
    
    // 任意のUniTaskを待つ
    var player = new Player("url");
    var tex = await player.PlayerInitAsync;
    Assert.That(tex,!Is.Null);
});

// 適当なクラス
class Player{
    private readonly AsyncLazy<Texture> textureLoadAsync;
    public UniTask PlayerInitAsync => textureLoadAsync.Task;
        
    // 非同期で初期化を行う
    public Player(string path){
        textureLoadAsync = UniTask.Lazy(async () => 
            await Addressables.LoadAssetAsync<Texture>(path)
        );
    }
}

screenshot_1639389500.png

ちゃんと成功してくれました。

UniTask.ToCoroutine はその名の通り UniTask から コルーチンに変換してくれるファクトリメソッドです。
テストメソッドを丸々 UniTask.ToCoroutine にすることで書き味もかなり良い感じです。

n秒待つ処理や、フレーム待機、 などなど、コルーチンではできなかったものもUniTaskなら待つことができます。(yield return null が使えるので AsyncOperation のテストはコルーチンでもできます)

なにより async/await でテストを書けるのでとても書き心地が良いです。

注意(2.3.1で修正済み)

こちらの内容は UniTask2.3.1(https://github.com/Cysharp/UniTask/releases/tag/2.3.1) にて正しく動くように修正されました。詳しくは UniTask.WaitForEndOfFrameが正しく動くようになった を見てください。

UniTask には await UniTask.WaitForEndOfFrame() というファクトリメソッドがあります。
コルーチンの yield return WaitForEndOfFrame の UniTask 実装ですが、実はこれちゃんと機能していません(UniTask2.2.5現在)
こちらの Issue(https://github.com/Cysharp/UniTask/issues/316) によれば、いずれは Obsolete になりそうな雰囲気です。
全ての描画が終わった後に処理をしたいときに使うものですが、PlayMode で使用しても正しく機能しないので注意が必要です。

/// <summary>
/// 例えばスクリーンショットを撮るテスト(コルーチン)
/// </summary>
[UnityTest]
public IEnumerator WaitForEndOfFrameIEnumeratorTest(){
    new GameObject("camera").AddComponent<Camera>();
    
    // EndOfFrameまで待つ
    yield return new WaitForEndOfFrame();
        
    var tex = new Texture2D(Screen.width, Screen.height);
    
    // // EndOfFrameでしか動かない
    tex.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
    tex.Apply();
    
    var sprite = Sprite.Create(
        tex,
        new Rect(0, 0, tex.width, tex.height),
        new Vector2(0.5f, 0.5f)
    );
    
    var renderer = new GameObject("sprite").AddComponent<SpriteRenderer>();
    renderer.sprite = sprite;
    yield return new WaitForSeconds(5);
}
/// <summary>
/// 例えばスクリーンショットを撮るテスト(UniTask)
/// </summary>
[UnityTest]
public IEnumerator WaitForEndOfFrameUniTaskTest() => UniTask.ToCoroutine(async () => {
    new GameObject("camera").AddComponent<Camera>();
    
    // EndOfFrameまで待つ・・・???
    await UniTask.WaitForEndOfFrame();
    
    var tex = new Texture2D(Screen.width, Screen.height);
    
    // // EndOfFrameでしか動かない
    tex.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
    tex.Apply();
    
    var sprite = Sprite.Create(
        tex,
        new Rect(0, 0, tex.width, tex.height),
        new Vector2(0.5f, 0.5f)
    );
    
    var renderer = new GameObject("sprite").AddComponent<SpriteRenderer>();
    renderer.sprite = sprite;
    await UniTask.Delay(5000);
});

上記の2つはカメラの映像をスクリーンショットするものです。
Texture2D.ReadPixels は PlayerLoop の EndOfFrame のタイミングでしか動きません。
これをPlayModeでテストするとコルーチンの方は綺麗なサンセットを映してくれますが、UniTask版はエラーがでて失敗してしまいます。

オマケ

UniTask の豊富なファクトリメソッドにはいつも感謝感謝です。
ToCoroutine があるおかげでテストを async/await で書くことができます。
ほかにも同じような用途で、sync ⇔ async の変換ができるファクトリメソッドをいくつか紹介します。

UniTask.Void

uGUIのイベントに async/await させたいメソッドを登録させることができます。

public class Sample : MonoBehaviour
{
    public void OnClickAction(int delay) => UniTask.Void(async () => {
        await UniTask.Delay(delay);
        Debug.Log("ディレイ");
    });
}

screenshot_1639395794.png

UniTask.UnityAction

同じような感じで onClick イベント等に登録できます

button.onClick.AddListener(UniTask.UnityAction(async ct => {
    await UniTask.Delay(1000, cancellationToken : ct);
    Debug.Log("ディレイ");
},this.GetCancellationTokenOnDestroy()));

UniTask.Action

そのままデリゲートに非同期処理を入れることができます

 Action action = UniTask.Action(async ct => {
     await UniTask.Delay(1000, cancellationToken : ct);
     Debug.Log("ディレイ");
 },this.GetCancellationTokenOnDestroy());

UniTask.Post

async/await にはしたくないけど PlayerLoop の実行タイミングを変えたいようなときに使えます

void Start() => UniTask.Post(() => {
    Debug.Log("FixedUpdate!");
},PlayerLoopTiming.FixedUpdate);
8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?