本記事は 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;
など限られたもののみテストが可能です。
柔軟な非同期処理をテストしたい場合は PlayMode でテストするしかありませんでした。
UniTask で非同期テストしよう!
UnityTestRunner で UniTask を使うときはテストフォルダの AssemblyDefinition に UniTask への参照を追加しましょう。
UniTask を使えるからと言って、テストメソッドの返り値を async UniTask
や async 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)
);
}
}
ちゃんと成功してくれました。
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("ディレイ");
});
}
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);