Unityでテストコードを書いてみた

Unityでテストコードを書いてみようと調べてみたところ、Unity Test Frameworkというものが標準で用意されているようなので、今回はこちらを試してみた。

実行環境

Windows 11 Pro
Unity 2022.3.7f1 LTS

Unity Test Framwork (Unity Test Runner)のインストール

まずはWindow>Package ManagerよりUnity Test Frameworkがインストールされていることを確認。

次にWIndow>Genral>Test Runnerを選択。

PlayModeとEditModeがあり、違いは下記の公式リンクを参照。自分の理解では、もともとUnityにはEdit ModeとPlay Modeの2種類があり、Edit Modeが普段開発を行っているモードで、Play Modeが開発したゲームを動かす際のモード。ここではどちらのモードでテストを行うか選択する。PlayModeだとGUIのテストが出来る(?)。

Edit Mode Test

Edit Mode Testを試すために、Create EditMode Test Assembly Folderを選択。プロジェクト内にTestsフォルダが作成され、その中にTests.asmdefが作成されるている。

プロジェクトのTestsフォルダ内で右クリック>Create>Testing>C# Test Scriptを選択すると、デフォルトのテストコードが生成され、中身は以下のような感じ。

この状態でTest Runnerからテストを選択して、右クリック>Runでテストが実行される。オールグリーンなので全てのテストケースが成功。

次にテスト対象の簡単なサンプルコードを書いて、テストを実行する。今回のテスト対象のコードがこちら。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SampleScript
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Sub(int a, int b)
    {
        return a - b;
    }

    public int Multi(int a, int b)
    {
        return a * b;
    }
}

そしてテストコードに読み込ませるためのアセンブリファイルを作成する。テスト対象のコードが入ったフォルダ(自分の場合はScriptsフォルダ)を右クリック>Create>Assembly Definition、作成したアセンブリファイルをTests.asmdefのInspectorからAssembly Definition Referencesに追加する。ScriptsAssemblyが作成したアセンブリファイルになる。

テストコードを作成し、テストを実行する。

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

public class SampleScriptEditTest
{

    [Test]
    public void SampleScriptEditTest_Add1()
    {
        SampleScript test = new SampleScript();
        Assert.That(test.Add(100, 200), Is.EqualTo(300));
    }

    [Test]
    public void SampleScriptEditTest_Sub1()
    {
        SampleScript test = new SampleScript();
        Assert.That(test.Sub(5, 2), Is.EqualTo(3));
    }

    [Test]
    public void SampleScriptEditTest_Multi1()
    {
        SampleScript test = new SampleScript();
        Assert.That(test.Multi(3, 2), Is.EqualTo(6));
    }
}

オールグリーンなので全てのテストケースが成功。

テスト対象のコードを変更して、わざとテストが失敗するケースを確認。Addメソッドの戻り値をa + bからa + aに変更。

public int Add(int a, int b)
{
    return a + a;
}

テスト結果がレッドになり、テストが失敗していることが確認出来る。

Play Mode Test

次にPlay Mode Testを試すのでPlayModeのタブを選択して、Create PlayMode Test Assembly Folderを選択。

PlayModeの場合は下記のコードをテストする。内容としては画面に配置したCapsuleオブジェクトが矢印キーの操作に応じて、前後左右に移動するコードとなっている。当初はデフォルトのInputを使用して、テスト対象のコードとテストコードを書こうとしたが、テストコードでキー操作を再現することが出来なかった。調べてみるとUnityには別途Input Systemというものがあり、そちらではキー操作をコードで再現できるようなので、そちらを利用するようにした。以下がテスト対象のコードになる。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Windows;

public class CapsuleController : MonoBehaviour
{
    InputAction move;
    private float vInput;
    private float hInput;
    private float moveSpeed = 10f;
    private Vector3 inputDirection;

    // Start is called before the first frame update
    void Start()
    {
        var playerInput = GetComponent<PlayerInput>();
        move = playerInput.actions["Move"];
    }

    // Update is called once per frame
    void Update()
    {
        //キー入力を取得
        var inputMoveAxis = move.ReadValue<Vector2>();
        inputDirection.x = inputMoveAxis.y;
        inputDirection.z = inputMoveAxis.x;

        vInput = inputDirection.x * moveSpeed;
        hInput = inputDirection.z * moveSpeed;
        this.transform.Translate(Vector3.forward * vInput * Time.deltaTime);
        this.transform.Translate(Vector3.right * hInput * Time.deltaTime);
    }
}

簡単にInput Systemの設定の流れだけを説明すると、まずPackage ManagerよりInput Systemをインストール。

次に.inputactionsファイルを作成し、キーの設定を行う。

キー操作を行いたいオブジェクトにAdd ComponentからPlayer Inputを追加。Actions とDefaultMapに先に作成した Input Actions とキー設定を追加。これでデフォルトのInputではなく、InputSystemを用いたキー操作が出来るようになる。

そして肝心のテストコードがこちら。

using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;

public class CapsuleControllerTest
{
    bool sceneLoading;
    GameObject testObject;
    CapsuleController targetScript;

    // OneTimeSetUp:全テストを実行する前に一度だけ処理する
    [OneTimeSetUp]
    public void InitializeTest()
    {
        sceneLoading = true;
        SceneManager.LoadSceneAsync("SampleScene").completed += _ => {
            testObject = GameObject.Find("Capsule");
            targetScript = testObject.GetComponent<CapsuleController>();
            sceneLoading = false;
            Debug.Log("Scene Load Complete");
        };
    }

    // SetUp:各テストの前に毎回処理する
    [SetUp]
    public void InitializeAllTest()
    {
        if (testObject != null) testObject.transform.position = Vector3.zero;
    }

    // Order:優先度を指定して最初にロードの完了待ちを行う
    [UnityTest]
    [Order(-100)]
    public IEnumerator LoadWait()
    {
        yield return new WaitWhile(() => sceneLoading);
    }

    // オブジェクト参照エラーをチェックするテスト
    [UnityTest]
    public IEnumerator CheckObject()
    {
        Assert.NotNull(testObject);
        yield return null;
    }

    // 右方向への移動を実行するテスト
    [UnityTest]
    public IEnumerator MoveRightTest()
    {
        var currentPos = testObject.transform.position;
        Keyboard keyboard = InputSystem.AddDevice<Keyboard>();
        InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.RightArrow));
        InputSystem.Update();
        yield return new WaitForSeconds(0.2f); // 微小な待機
        var movedPos = testObject.transform.position;
        Assert.Greater(movedPos.x, currentPos.x);

        // キーボードデバイスを無効にする
        InputSystem.DisableDevice(keyboard);
        yield return null;
    }

    // 左方向への移動を実行するテスト
    [UnityTest]
    public IEnumerator MoveLeftTest()
    {
        var currentPos = testObject.transform.position;
        Keyboard keyboard = InputSystem.AddDevice<Keyboard>();
        InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.LeftArrow));
        InputSystem.Update();
        yield return new WaitForSeconds(0.2f); // 微小な待機
        var movedPos = testObject.transform.position;
        Assert.Less(movedPos.x, currentPos.x);

        // キーボードデバイスを無効にする
        InputSystem.DisableDevice(keyboard);
        yield return null;
    }

    // 前方向への移動を実行するテスト
    [UnityTest]
    public IEnumerator MoveForwardTest()
    {
        var currentPos = testObject.transform.position;
        Keyboard keyboard = InputSystem.AddDevice<Keyboard>();
        InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.UpArrow));
        InputSystem.Update();
        yield return new WaitForSeconds(0.2f); // 微小な待機
        var movedPos = testObject.transform.position;
        Assert.Greater(movedPos.z, currentPos.z);

        // キーボードデバイスを無効にする
        InputSystem.DisableDevice(keyboard);
        yield return null;
    }

    // 後ろ方向への移動を実行するテスト
    [UnityTest]
    public IEnumerator MoveBackwardTest()
    {
        var currentPos = testObject.transform.position;
        Keyboard keyboard = InputSystem.AddDevice<Keyboard>();
        InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.DownArrow));
        InputSystem.Update();
        yield return new WaitForSeconds(0.2f); // 微小な待機
        var movedPos = testObject.transform.position;
        Assert.Less(movedPos.z, currentPos.z);

        // キーボードデバイスを無効にする
        InputSystem.DisableDevice(keyboard);
        yield return null;
    }
}

最後にテストを実行するためにはEdit Modeと同様にアセンブリファイルを作成して、Assembly Definition Referencesに追加する必要がある。一つ注意が必要なのはEditModeと同様にアセンブリファイルを作成すると、何故か作成する前にはなかったエラーが発生するようになる。

そのため作成したアセンブリファイルにUnity.InputSystemのAssembly Definition Referencesを追加する必要があった。

またテストのアセンブリファイルにもUnity.InputSystemを追加する必要がある。

このままテストを実行すると失敗するため、File>Build Settingsからテスト対象のシーンをScenes in Buildに追加する必要がある。

いよいよPlayModeのテストを実行する。実際にCapsuleが自動で前後左右に動いてテストが実行され、全てのテストケースが成功していることがわかる。

参考リンク


いいなと思ったら応援しよう!