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が自動で前後左右に動いてテストが実行され、全てのテストケースが成功していることがわかる。