Unity
UniRx
Zenject
UnityTestRunner

Unity使いは全員Unity Test Runnerを使え!爆速のトライ&エラー環境だぞ!

前書き

Unityを使っている人は

  • 拾ってきたサンプルコードの挙動を確認したい
  • 「書いたはいいが自信がないコード」の実際の動きを確認したい
  • UniRxとかZenjectとか難しいライブラリ/フレームワークを使ったコードを書いていろいろ実験したい

と思ったときに、いったいどうしているだろうか?

  • 既存プロジェクトの適当なMonoBehaivour継承クラスに間借りしてコードを書いてプレイモードで確認?
  • [RuntimeInitializeOnLoadMethod]をつけたメソッドを書いてプレイモード開始と同時に実行して確認?
  • csharppad で実行して確認?

まったくもってナンセンス。時間のムダ!

Unity Test Runnerを使え!「テストコードとかめんどくさくて書く気ないし」とか関係ない!

今すぐ使え!

なんで?

csharppad ではUnityやプロジェクトのコンテキストでコードを実行できないので論外として、それ以外の確認方法についての話。

  1. コードを書く
  2. 再生ボタンを押してプレイモードに入る
  3. コードの挙動を確認する
  4. 再生ボタンを押してプレイモードを終了する
  5. コードを修正し2.へ(以下ループ)

このサイクルにおけるそれぞれのステップにかかる時間的コストはかなり大きい。

コードを書いたらコンパイルによる待ち時間が発生する(イライラする)[1]。プレイモードの開始には謎の待ち時間が発生する。アホみたいにかかる(かなりイライラする)。アプリケーションの状態に依存するようなコードの場合、3で実際にコードが動く画面まで遷移させたりする必要がある(この場合はそもそもちゃんとテストコードを書くべき、イライラする)。プレイモードの終了にも謎の待ち時間が発生する。微妙にかかる。

また、既存のコード/コンテキストに影響を受けて、それ単体で得られるはずだった結果とは異なる結果になるかもしれない。

Unity Test Runnerを使うと実行したいコードのコンテキストを局所化でき、2,4にかかる時間を0にしてトライ&エラーを爆速にできる可能性がある。

(実行にかかった時間も自動で計測・表示されるので、簡易的なパフォーマンス計測にも利用できる)

Unity Test Runnerってなに?

Unityのテスト実行ツールで、NUnit という.NET用テスティングフレームワークが使われている。

TestRunner001.png
グローバルメニュー→「Window」→「Test Runner」でウィンドウを開く

テストコードを書くとウィンドウに一覧で表示してくれて、

  • 全部実行
  • 選択して実行
  • 特定のカテゴリのテストだけ実行
  • 選択したプラットフォーム向けにビルドしたプレイヤー上で実行

などができる。ワンクリック(もしくはダブルクリック)で実行可能。

TestRunner002.png
「Run All」「Run Selected」などのボタンがあり、使い方は雰囲気でわかる

使い方に関する詳細は以下を参考に

最初はテストの機能を有効にするために若干のクリック操作が必要、マニュアル読むべし。

設定が済んだら後はクラスファイルを新しく作ってテストコードを書くだけ。とりあえず後述のサンプルクラスをそのままコピペして実行してみて、雰囲気を掴むといい。

テストは実行する環境によってEditModeテストPlayModeテストの2種類がある。

2種類のテスト

EditModeテスト,PlayModeテスト共通

  • 属性Attrubuteは基本的にNUnitのものを利用する
  • テストコードとして一覧表示/実行したいメソッドに[Test]もしくは[UnityTest]をつける[2]
    • [Test]は同期的に実行される
      • 戻り値の型は必ずvoid
    • [UnityTest]はコルーチンとして実行される
      • 戻り値の型は必ずIEnumerator
      • メソッド内でyield returnが使える(モードによって一部制限あり)
    • [Test],[UnityTest]などを含むクラスファイルがEditor/以下にあるならEditModeテスト、そうでなければPlayModeテストと認識される
  • テストコードを専用の別クラスに分けてもいいし、テストしたいクラス内に直接書くこともできる
  • NUnitと違ってテストコードを記述したクラスに[TestFixture]をつける必要はない
using System.Collections;
using NUnit.Framework;
using UnityEngine.TestTools;
using Assert = UnityEngine.Assertions.Assert; // AssertはNUnitのではなくUnity製のものを使う

public class NewEditModeTest
{
    [Test]
    public void SimplePasses()
    {
        Assert.AreEqual(2, 1 + 1);
    }

    [UnityTest]
    public IEnumerator EnumeratorPasses()
    {
        yield return null;
        Assert.AreEqual(2, 1 + 1);
    }
}

コードの挙動を確認したいだけの場合はAssertを使わなくてもDebug.Log()やブレークポイントによるステップ実行でも十分。

ただ、Assertを使うと期待した値でなかったときに表示がわかりやすいなどいくつかメリットがあるので、できればAssertを使う。

Unityで使えるAssert

  • UnityEngine.Assertions.Assert
  • NUnit.Framework.Assert

の2つがあるが、基本的にはUnityのものを使う[3]

Assertチートシート

UnityEngine.Asertions.Assertのメソッドをまとめた。ちなみに引数を2つ取る場合は(expected, actual)の順番。

メソッド テスト内容
AreEqual(a,b) a == b かどうか
AreNotEqual(a,b) a != かどうか
IsTrue(a) a == true かどうか
IsFalse(a) a == false かどうか
IsNull(a) a == null かどうか
IsNotNull() a != null かどうか
AreApproximatelyEqual(a,b) ほぼ同じ(誤差0.00001f以内)かどうか
AreNotApproximatelyEqual(a,b) ↑のNOT

※詳細はリファレンスを参照
※NUnitのものと違い、UnityのAssertにはThat()などはない(貧弱ゥ!)

EditModeテスト

  • テストコードを記述したクラスファイルはEditor/以下に置く必要がある
  • プレイモードを経由せず、エディタ上で即実行される
    • プレイモード移行のオーバーヘッドがなく、爆速で実行できる
    • MonoBehaviourのテストは実質不可(Start(),Update()などが呼ばれない)
  • [UnityTest]yield returnにはnullしか渡せない
    • 故に非同期のテストは実行不可、おそらく使わない
Assets/Tests/EditMode/Editor/ZenjectTest.cs
using NUnit.Framework;
using Zenject;
using Assert = UnityEngine.Assertions.Assert;

// Zenjectの挙動確認を行う(あらかじめZenjectの導入が必要)
public class ZenjectTest
{
    [Test]
    public void ResolveTest()
    {
        var container = new DiContainer();
        container.BindInstance("hoge");

        Assert.AreEqual("hoge", container.Resolve<string>());
    }

    [Test]
    public void ResolveAllTest()
    {
        var container = new DiContainer();
        container.BindInstance("hoge");
        container.BindInstance("fuga");

        var all = container.ResolveAll<string>();

        Assert.AreEqual(2, all.Count);
        Assert.AreEqual("hoge", all[0]);
        Assert.AreEqual("fuga", all[1]);
    }
}

EditModeテストは爆速で回せるので基本的にこっちを使えるように工夫する。

例えば Unity API に関係ないロジックはMonoBehaviour継承クラスに直接記述するのではなくC#のピュアなクラス内に実装することでEditModeテストを可能にしつつ、必要があればMonoBehaviour継承クラスにインスタンスとして持たせるようにする。

また、

  • UniRxの各オペレータ/ストリームの挙動
  • ZenjectのピュアなC#用機能の挙動

など、ライブラリ/フレームワークの「Unityのヒエラルキーに依存しない機能」の学習/確認にはEditModeテストが最適。

その際、確認した挙動にはコメントを追記してサンプルコードとして残すことによって、プロジェクトの他のメンバーの学習の手助けとすることができる。

テストコードだと最小構成で実行できるうえにわからない/気になる部分を編集して即実行して試せるのが大変良い。

PlayModeテスト

  • テストコードを記述したクラスファイルはEditor/以下ではない場所に置く必要がある
  • テストコードは自動生成されたテスト用のシーン上で実行される(通常のプレイモードと同じくらい時間がかかる
    • MonoBehaviourのテストが可能
    • 1. テスト用シーンファイルが自動生成される
    • 2. プレイモードへ移行
    • 3. テストコード実行(複数のテストを実行する場合はすべて実行)
    • 4. プレイモード終了
    • 5. テスト用シーンが自動削除される
  • [UnityTest]yield returnが特に制限なく使える。非同期テストしたい場合はこっち。
  • 個々のテスト実行後の破棄処理を適切に行わないと他のテストに影響が出る可能性がある
    • 1テストごとに呼ばれる[SetUp](テスト前),[TearDown](テスト後)で適切に破棄処理を行う
  • ビルドしたプレイヤー上で実行可能(すごそう、試してない😅)
Assets/Scripts/HogeComponent.cs
using UnityEngine;

// 適当なコンポーネントを定義
public class HogeComponent : MonoBehaviour
{
    public int count { get; private set; }

    void Start()
    {
        this.count = 1;
    }
}
Assets/Tests/PlayMode/HogeComponentTest.cs
using System.Collections;
using UnityEngine;
using UnityEngine.TestTools;
using Assert = UnityEngine.Assertions.Assert;

// テスト用クラス
public class HogeComponentTest
{
    [UnityTest]
    public IEnumerator CountTest()
    {
        var go = new GameObject("Hoge");
        var hoge = go.AddComponent<HogeComponent>();

        // Start()前
        Assert.AreEqual(0, hoge.count);

        yield return null;

        // Start()後
        Assert.AreEqual(1, hoge.count);
    }
}

EditModeと比べて実行に時間がかかるが、非同期テスト/MonoBehaviour関連テストはPlayModeテストとして書く必要がある。

まとめ

力尽きたのでそのうち書く。テストコードをちゃんと書くための記事も書く

 


  1. コンパイル時間をなるべく短くする方法もある。 テラシュールブログ の記事参照。 

  2. 他にも[TestCase],[TestCaseSource)]などがあり、いずれも同期的に実行される。 

  3. Unity向けに最適化されていて、本番ビルド時にストリッピングされる。詳細は Unity公式ブログ の記事を参照。