UnityTestRunnerでUniTaskを使おう！

本記事は **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` にします。

```c#
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/9510831b-c025-0f7a-d9dc-69ea6b144d37.png)

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

# UniTask で非同期テストしよう！

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

![screenshot_1638902743.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/bccb6f3e-0431-9a05-a1a7-a0c840726fdb.png)


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

# UniTask.ToCoroutine

```c#
[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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/c0cf2e80-27df-2841-70b7-47216300fc11.png)

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

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

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

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


# 注意(2.3.1で修正済み)

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

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

```c#
/// <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);
}
```

```c#
/// <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 させたいメソッドを登録させることができます。

```c#
public class Sample : MonoBehaviour
{
    public void OnClickAction(int delay) => UniTask.Void(async () => {
        await UniTask.Delay(delay);
        Debug.Log("ﾃﾞｨﾚｲ");
    });
}
```

![screenshot_1639395794.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/59b066e0-6df0-662d-1ac0-d18d36d955ac.png)

## UniTask.UnityAction

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

```c#
button.onClick.AddListener(UniTask.UnityAction(async ct => {
    await UniTask.Delay(1000, cancellationToken : ct);
    Debug.Log("ﾃﾞｨﾚｲ");
},this.GetCancellationTokenOnDestroy()));
```

## UniTask.Action

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

```c#
 Action action = UniTask.Action(async ct => {
     await UniTask.Delay(1000, cancellationToken : ct);
     Debug.Log("ﾃﾞｨﾚｲ");
 },this.GetCancellationTokenOnDestroy());
```

## UniTask.Post

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

```c#
void Start() => UniTask.Post(() => {
    Debug.Log("FixedUpdate!");
},PlayerLoopTiming.FixedUpdate);
```