PlayModeテストとは?

PlayModeテストは、Unity のゲーム実行環境でテストを実行する機能です。

EditModeテストとは異なり、実際にゲームを起動した状態でテストを行うため、MonoBehaviourのライフサイクルメソッド(Start, Updateなど)やGameObjectの動作、物理演算などを検証できます。

ゲームの実際の挙動をテストする必要がある場合に、PlayModeテストが活躍します。

PlayModeテストが必要な場面

PlayModeテストは、以下のような場面で使用します。

MonoBehaviourのライフサイクルをテストする

Awake(), Start(), Update()などのライフサイクルメソッドは、EditModeでは自動的に呼ばれません。これらのメソッドの動作を確認するには、PlayModeテストが必要です。

GameObjectやコンポーネントの動作確認

実際にGameObjectを生成し、コンポーネントを追加して、その動作をテストできます。

時間経過を伴う処理のテスト

複数フレームにわたる処理や、時間経過による状態変化をテストできます。

コルーチンのテスト

StartCoroutine()で実行するコルーチンの動作を検証できます。

物理演算のテスト

RigidbodyColliderを使った物理演算の挙動をテストできます。

基本的なPlayModeテストの書き方

PlayModeテストでは、[UnityTest]属性とIEnumeratorを使用します。

シンプルなPlayModeテスト

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

public class SimplePlayModeTest
{
    [UnityTest]
    public IEnumerator GameObjectCreation_CreatesGameObject()
    {
        // Arrange & Act
        var gameObject = new GameObject("TestObject");

        // 1フレーム待機
        yield return null;

        // Assert
        Assert.IsNotNull(gameObject);
        Assert.AreEqual("TestObject", gameObject.name);

        // クリーンアップ
        Object.Destroy(gameObject);
    }
}

重要なポイント:

  • [UnityTest]属性を使用([Test]ではない)
  • 戻り値の型はIEnumerator
  • yield return nullで1フレーム待機
  • テスト後はObject.Destroy()でオブジェクトを削除

実装例

MonoBehaviourのテスト

MonoBehaviourのライフサイクルメソッドをテストします。

テスト対象のクラス:

using UnityEngine;

public class Counter : MonoBehaviour
{
    public int count = 0;
    public bool isStarted = false;

    private void Start()
    {
        isStarted = true;
        count = 10;
    }

    private void Update()
    {
        count++;
    }

    public void Reset()
    {
        count = 0;
    }
}

テストコード:

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

public class CounterTest
{
    [UnityTest]
    public IEnumerator Start_InitializesCountToTen()
    {
        // Arrange
        var gameObject = new GameObject();
        var counter = gameObject.AddComponent<Counter>();

        // Act
        // 1フレーム待機してStart()が呼ばれるのを待つ
        yield return null;

        // Assert
        Assert.IsTrue(counter.isStarted);
        Assert.AreEqual(10, counter.count);

        // Cleanup
        Object.Destroy(gameObject);
    }

    [UnityTest]
    public IEnumerator Update_IncrementsCountEveryFrame()
    {
        // Arrange
        var gameObject = new GameObject();
        var counter = gameObject.AddComponent<Counter>();

        // Start()が呼ばれるまで待機
        yield return null;

        int initialCount = counter.count;

        // Act
        // 3フレーム待機
        yield return null;
        yield return null;
        yield return null;

        // Assert
        Assert.AreEqual(initialCount + 3, counter.count);

        // Cleanup
        Object.Destroy(gameObject);
    }

    [UnityTest]
    public IEnumerator Reset_SetsCountToZero()
    {
        // Arrange
        var gameObject = new GameObject();
        var counter = gameObject.AddComponent<Counter>();
        yield return null;

        // Act
        counter.Reset();

        // Assert
        Assert.AreEqual(0, counter.count);

        // Cleanup
        Object.Destroy(gameObject);
    }
}

時間経過を伴うテスト

タイマー機能をテストします。

テスト対象のクラス:

using UnityEngine;

public class Timer : MonoBehaviour
{
    public float duration = 5f;
    public bool isFinished = false;
    private float _elapsedTime = 0f;

    private void Update()
    {
        if (isFinished) return;

        _elapsedTime += Time.deltaTime;

        if (_elapsedTime >= duration)
        {
            isFinished = true;
        }
    }

    public float GetElapsedTime()
    {
        return _elapsedTime;
    }
}

テストコード:

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

public class TimerTest
{
    [UnityTest]
    public IEnumerator Timer_CompletesAfterDuration()
    {
        // Arrange
        var gameObject = new GameObject();
        var timer = gameObject.AddComponent<Timer>();
        timer.duration = 1f;

        yield return null; // Start()を待つ

        // Act
        // 1秒待機
        yield return new WaitForSeconds(1f);

        // Assert
        Assert.IsTrue(timer.isFinished);
        Assert.GreaterOrEqual(timer.GetElapsedTime(), 1f);

        // Cleanup
        Object.Destroy(gameObject);
    }

    [UnityTest]
    public IEnumerator Timer_NotFinishedBeforeDuration()
    {
        // Arrange
        var gameObject = new GameObject();
        var timer = gameObject.AddComponent<Timer>();
        timer.duration = 2f;

        yield return null;

        // Act
        yield return new WaitForSeconds(0.5f);

        // Assert
        Assert.IsFalse(timer.isFinished);

        // Cleanup
        Object.Destroy(gameObject);
    }
}

コルーチンのテスト

コルーチンの動作をテストします。

テスト対象のクラス:

using System.Collections;
using UnityEngine;

public class DelayedAction : MonoBehaviour
{
    public bool actionExecuted = false;

    public void StartDelayedAction(float delay)
    {
        StartCoroutine(ExecuteAfterDelay(delay));
    }

    private IEnumerator ExecuteAfterDelay(float delay)
    {
        yield return new WaitForSeconds(delay);
        actionExecuted = true;
    }
}

テストコード:

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

public class DelayedActionTest
{
    [UnityTest]
    public IEnumerator DelayedAction_ExecutesAfterDelay()
    {
        // Arrange
        var gameObject = new GameObject();
        var delayedAction = gameObject.AddComponent<DelayedAction>();

        yield return null;

        // Act
        delayedAction.StartDelayedAction(0.5f);

        // 実行前の確認
        Assert.IsFalse(delayedAction.actionExecuted);

        // 0.5秒待機
        yield return new WaitForSeconds(0.5f);

        // Assert
        Assert.IsTrue(delayedAction.actionExecuted);

        // Cleanup
        Object.Destroy(gameObject);
    }
}

Physicsのテスト

物理演算を使ったテストの例です。

テスト対象のクラス:

using UnityEngine;

public class Projectile : MonoBehaviour
{
    private Rigidbody _rigidbody;

    private void Awake()
    {
        _rigidbody = GetComponent<Rigidbody>();
    }

    public void Launch(Vector3 force)
    {
        _rigidbody.AddForce(force, ForceMode.Impulse);
    }
}

テストコード:

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

public class ProjectileTest
{
    [UnityTest]
    public IEnumerator Launch_MovesProjectileUpward()
    {
        // Arrange
        var gameObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        var rigidbody = gameObject.AddComponent<Rigidbody>();
        var projectile = gameObject.AddComponent<Projectile>();

        yield return null; // Awake()を待つ

        Vector3 initialPosition = gameObject.transform.position;

        // Act
        projectile.Launch(Vector3.up * 10f);

        // 物理演算を進める
        yield return new WaitForFixedUpdate();
        yield return new WaitForFixedUpdate();

        // Assert
        Assert.Greater(gameObject.transform.position.y, initialPosition.y);

        // Cleanup
        Object.Destroy(gameObject);
    }

    [UnityTest]
    public IEnumerator Launch_AppliesForceToRigidbody()
    {
        // Arrange
        var gameObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        var rigidbody = gameObject.AddComponent<Rigidbody>();
        var projectile = gameObject.AddComponent<Projectile>();

        yield return null;

        // Act
        projectile.Launch(Vector3.forward * 5f);
        yield return new WaitForFixedUpdate();

        // Assert
        Assert.Greater(rigidbody.velocity.magnitude, 0f);

        // Cleanup
        Object.Destroy(gameObject);
    }
}

テストシーンの活用

複雑なテストでは、専用のテストシーンを作成すると便利です。

テストシーンの作成

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

public class SceneBasedTest
{
    [UnitySetUp]
    public IEnumerator SetUp()
    {
        // テストシーンを読み込む
        yield return SceneManager.LoadSceneAsync("TestScene", LoadSceneMode.Single);
    }

    [UnityTest]
    public IEnumerator TestScene_ContainsPlayer()
    {
        // シーン内のオブジェクトを検索
        var player = GameObject.Find("Player");

        // Assert
        Assert.IsNotNull(player);

        yield return null;
    }

    [UnityTearDown]
    public IEnumerator TearDown()
    {
        // テスト後のクリーンアップ
        yield return SceneManager.LoadSceneAsync("EmptyScene", LoadSceneMode.Single);
    }
}

プレハブを使ったテスト

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

public class PrefabTest
{
    [UnityTest]
    public IEnumerator LoadPrefab_InstantiatesCorrectly()
    {
        // Arrange
        var prefab = Resources.Load<GameObject>("Prefabs/Enemy");
        Assert.IsNotNull(prefab, "プレハブが見つかりません");

        // Act
        var instance = Object.Instantiate(prefab);
        yield return null;

        // Assert
        Assert.IsNotNull(instance);
        Assert.IsNotNull(instance.GetComponent<Enemy>());

        // Cleanup
        Object.Destroy(instance);
    }
}

よく使うテストパターン

フレーム待機のパターン

// 1フレーム待機
yield return null;

// 複数フレーム待機
for (int i = 0; i < 10; i++)
{
    yield return null;
}

物理演算の待機

// FixedUpdate 1回分待機
yield return new WaitForFixedUpdate();

// 物理演算を複数回進める
for (int i = 0; i < 5; i++)
{
    yield return new WaitForFixedUpdate();
}

時間待機

// 実時間で待機
yield return new WaitForSeconds(1f);

// スケールされた時間で待機
yield return new WaitForSecondsRealtime(1f);

条件が満たされるまで待機

// 条件が真になるまで待機
yield return new WaitUntil(() => enemy.isDead);

// 条件が偽になるまで待機
yield return new WaitWhile(() => player.isMoving);

タイムアウト付き待機

[UnityTest]
public IEnumerator WaitForCondition_WithTimeout()
{
    var gameObject = new GameObject();
    var component = gameObject.AddComponent<MyComponent>();

    yield return null;

    // 最大5秒待機
    float timeout = 5f;
    float elapsed = 0f;

    while (!component.isReady && elapsed < timeout)
    {
        yield return null;
        elapsed += Time.deltaTime;
    }

    Assert.IsTrue(component.isReady, "タイムアウト: 条件が満たされませんでした");

    Object.Destroy(gameObject);
}

注意点

PlayModeテストは実行時間が長い

PlayModeテストは、実際にゲームを起動するため、EditModeテストよりも実行時間が長くなります。

頻繁に実行するテストはEditModeに、GameObjectや物理演算が必要なテストのみPlayModeにすることで、テスト全体の実行時間を短縮できます。

テストの独立性を保つ

各テストは独立して実行できる必要があります。テスト間で状態が共有されないよう、適切にクリーンアップを行いましょう。

[UnityTest]
public IEnumerator MyTest()
{
    // Arrange
    var gameObject = new GameObject();

    yield return null;

    // Act & Assert
    // テストのコード

    // Cleanup(必ず実行)
    Object.Destroy(gameObject);
}

TearDownの活用

複数のテストで共通のクリーンアップ処理がある場合は、[UnityTearDown]を使用します。

public class MyPlayModeTest
{
    private GameObject _testObject;

    [UnitySetUp]
    public IEnumerator SetUp()
    {
        _testObject = new GameObject("TestObject");
        yield return null;
    }

    [UnityTearDown]
    public IEnumerator TearDown()
    {
        if (_testObject != null)
        {
            Object.Destroy(_testObject);
        }
        yield return null;
    }

    [UnityTest]
    public IEnumerator Test1()
    {
        Assert.IsNotNull(_testObject);
        yield return null;
    }

    [UnityTest]
    public IEnumerator Test2()
    {
        Assert.AreEqual("TestObject", _testObject.name);
        yield return null;
    }
}

Time.timeScaleに注意

Time.timeScaleを変更すると、WaitForSecondsなどの待機時間に影響します。テスト後は必ず元に戻しましょう。

[UnityTest]
public IEnumerator TimeScale_Test()
{
    // 元の値を保存
    float originalTimeScale = Time.timeScale;

    try
    {
        // 時間を加速
        Time.timeScale = 2f;

        // テストのコード
        yield return new WaitForSeconds(1f); // 実際は0.5秒

        Assert.Pass();
    }
    finally
    {
        // 必ず元に戻す
        Time.timeScale = originalTimeScale;
    }
}

まとめ

PlayModeテストは、GameObjectMonoBehaviourの実際の動作をテストするための強力なツールです。

EditModeテストでは検証できない、ライフサイクルメソッドや物理演算、コルーチンなどをテストできます。実行時間は長くなりますが、ゲームの挙動を確実に検証するためには欠かせません。

テストの使い分け:

  • EditMode: ロジック、計算、データクラスなどの高速テスト
  • PlayMode: GameObjectMonoBehaviour、物理演算などの実行環境が必要なテスト

両方を組み合わせて使用することで、包括的なテストを実現できます。

ぜひプロジェクトにPlayModeテストを導入して、品質の高いゲーム開発を実現してください!