[Unity] 実行中動作の自動テスト方法

PlayModeテストは、Unityのテストフレームワークの一部であり、ゲームが実際にプレイされている状態(Play Mode)での挙動を自動的にテストするために使用されます。これは、ゲームのシーンが実行され、ゲームオブジェクトが実際に動いている状況でのテストを可能にし、ゲームの動作や特定のフィーチャーが期待通りに動作するかを確認するために役立ちます。

PlayerMode

PlayModeテストの特徴

  • 実行環境: PlayModeテストは、UnityエディターがPlay Modeにあるときに実行されます。これにより、ゲームの実際の実行環境でテストが行われ、ゲームオブジェクトのインスタンス化、シーンの読み込み、アニメーション、物理演算など、実行時の挙動をテストできます。
  • 非同期処理のテスト: PlayModeテストは、IEnumeratorを返すメソッドを利用して、コルーチンを使用することができます。これにより、非同期処理や、特定の時間を待機する処理(例えば、アニメーションの完了や非同期のロード処理など)のテストが可能になります。
  • ユーザーインタラクションのシミュレーション: プレイヤーの入力や操作に反応するロジックをテストする場合に有用です。例えば、仮想的なキープレスやマウスクリックをシミュレートして、それに対するゲームの反応をテストすることができます。

PlayModeテストの作成方法

  1. UnityTest属性を使用する: PlayModeテストメソッドには[UnityTest]属性を使用します。これは、テストランナーに対して、該当のメソッドがPlayModeで実行されるテストであることを示します。
  2. IEnumeratorを返す: PlayModeテストメソッドはIEnumeratorを返す必要があり、yield return null;を使ってフレームをスキップしたり、yield return new WaitForSeconds(seconds);を使って特定の時間だけ処理を待機させたりすることができます。
  3. Assertクラスを使用する: NUnitのAssertクラスを使用して、テストの条件を指定します。例えば、あるアクションが期待した結果を生んだかどうかを検証します。

注意点

  • PlayModeテストは実行に時間がかかることがあります。これは、シーンのロードや非同期処理の待機など、実行時の状態を再現するためです。
  • EditModeテストとPlayModeテストを適切に分けることが重要です。単純なロジックや関数のテストはEditModeテストで十分な場合が多く、PlayModeテストは実際のゲームプレイに近い環境でのテストに特化させるべきです。

準備

Unity Test Frameworkをインストールする方法は、Unityのドキュメントやリソースに従って行うことができます。一般的には、Unity開発環境にシームレスに統合されるいくつかのステップが含まれます。以下は、その基本的な手順の概要です:

  1. Unityプロジェクトを開く:テストを含めたいUnityプロジェクトを開きます。
  2. パッケージマネージャーにアクセスする
    • Unityエディタで、Window(ウィンドウ) > Package Manager(パッケージマネージャー) を選択します。これでパッケージマネージャーウィンドウが開きます。
  3. Test Frameworkパッケージを探す
    • パッケージマネージャーで、"Unity Registry"を選択してUnityが提供するすべての利用可能なパッケージを表示する必要があるかもしれません。"Test Framework"を検索します。
  4. Test Frameworkをインストールする
    • Test Frameworkパッケージを見つけたら、それをクリックしてから"Install(インストール)"ボタンをクリックします。Unityがインストールプロセスを処理します。
  5. テストの設定
    • インストール後、Window(ウィンドウ) > General(一般) > Test Runner(テストランナー) に移動してテストランナーウィンドウを開き、テストの作成、管理、実行を行うことができます。
    • EditModeテスト(エディター内で実行されるテスト)またはPlayModeテスト(プレイモードで実行されるテスト)を書くオプションがあります。
  6. テストの作成
    • テストを書くには、プロジェクト内に新しいC#スクリプトを作成します。通常、Testsなどのフォルダ内に作成すると整理しやすくなります。
    • NUnitフレームワークの構文を使用してテストを構造化します。Unity Test FrameworkはNUnitに基づいて構築されているため、テストメソッドの[Test]、セットアップメソッドの[SetUp]など、NUnitの属性をサポートしています。
  7. テストの実行
    • テストランナーウィンドウを使用してテストを実行します。すべてのテストを実行することも、特定のテストを選択して実行することもできます。テストランナーは、どのテストが合格したか、または失敗したかについてのフィードバックを提供します。
  8. 結果を確認する
    • テストを実行した後、テストランナーウィンドウで結果を確認します。失敗したテストを調査して、何が問題だったのかを理解し、コードまたはテストに必要な調整を行います。

このプロセスにより、Unityプロジェクトにテストを統合し、ゲームやアプリケーションが期待通りに動作することを確認するための単体テストを作成して実行することができます。自動テストは、開発プロセスの早い段階でエラーを捕捉することで、プロジェクトの品質と安定性を大幅に向上させることができます。

テストの環境

敵に見立てた球(Sphere)とプレイヤーに見立てたカプセル(Capsel)をシーンに配置しています

実行結果

Test Runner

自動テスト結果

ゲーム実行

エディターを実行し、上向きカーソルでプレイヤーを移動

コード

テスト評価用コードの概要

1. TriggerEventDetectorクラス

  • UnityEventを公開し、Unityのエディタからイベントリスナーをアタッチ可能にします。
  • OnTriggerStayメソッドを使用して、オブジェクトがトリガーコライダー内に滞在している間、特定のイベント(この場合はOnEnterイベント)を発火させます。

2. MockInputProviderクラス

  • テストや特定の入力シミュレーションに使用するための入力プロバイダーのモックを提供します。
  • GetAxisメソッドをオーバーライドして、指定された軸名に対する入力値(value)を返します。

3. Initializerクラス

  • ゲーム開始時に、プレイヤーの入力プロバイダーを設定し、特定のイベントリスナーをトリガーオブジェクトにアタッチします。
  • OnEnterFromEnemyメソッドは、プレイヤーが敵(この場合はトリガーオブジェクト)に触れた時に呼び出されるアクションを定義します。

4. PlayerControllerクラス

  • プレイヤーの移動やアクションを制御するためのクラスです。
  • SetInputProviderメソッドで入力プロバイダーを設定し、Updateメソッド内でプレイヤーの移動を処理します。

5. IInputProviderインターフェイス

  • 入力プロバイダーが実装すべき基本的なインターフェイスです。GetAxisメソッドを通じて、特定の軸に対する入力値を取得します。

6. ユニットテスト

  • PreConfiguredSceneTestクラスを使用して、プレイヤーが移動するとイベントが正しくトリガーされるかをテストします。
  • シーンを非同期でロードし、指定された条件下でのプレイヤーの挙動やイベントの発火を検証します。

この実装例は、Unityでのイベント駆動型プログラミングの基本を理解し、ゲーム内でのオブジェクト間の相互作用を管理する方法を学ぶのに役立ちます。

TriggerEventDetector

トリガーイベントの検出と処理

このコードは、Unityのイベントシステムを利用して特定のイベントを検出する機能を提供します。クラス内では、UnityEvent型のOnEnterという公開フィールドを定義し、これによりUnityのエディタからイベントリスナーをアタッチできるようになっています。OnTriggerStayメソッドは、他のオブジェクトがこのオブジェクトのトリガーコライダー内に滞在している間中、継続的に呼び出されます。このメソッド内でOnEnter.Invoke()を呼び出すことで、OnEnterイベントにアタッチされた全てのリスナーにイベントが発火されます。これにより、ゲームオブジェクトが特定のエリアにいる間に特定のアクションを実行するなど、柔軟なイベント駆動型のプログラミングが可能になります。

using UnityEngine;
using UnityEngine.Events; // UnityEventに必要

public class TriggerEventDetector : MonoBehaviour
{
    // UnityEventを公開して、エディタからイベントリスナーをアタッチできるようにする
    public UnityEvent OnEnter;

    private void OnTriggerStay(Collider other)
    {
        // UnityEventのInvokeメソッドを呼び出して、イベントを発火させる
        OnEnter.Invoke();
    }
}

IInputProvider

入力管理のためのインターフェース設計

このコードは、ゲーム内でユーザーの入力を管理するためのインターフェースを定義しています。IInputProviderインターフェースは、GetAxisというメソッドを含んでおり、これは特定の軸名を受け取って、その軸の入力値(通常は-1から1の間の浮動小数点数)を返します。例えば、プレイヤーが左右に動かすための入力や、上下に視点を移動するための入力を取得するのに使うことができます。このような設計により、異なる入力デバイスや入力方式でも、ゲームのコントローラー部分を柔軟に対応させることが可能になります。

public interface IInputProvider
{
    float GetAxis(string axisName);
}

MockInputProvider

入力値模倣によるテストの簡素化

これはIInputProviderインターフェースを実装し、入力値を模倣するために使用されます。コンストラクタでは、valueフィールドに初期値を設定し、GetAxisメソッドを通じてこの値を取得することができます。このクラスは、特定の入力値をテストする際に役立ちます。

public class MockInputProvider : IInputProvider
{
    private float value;

    public MockInputProvider(float value)
    {
        this.value = value;
    }

    public float GetAxis(string axisName)
    {
        return value;
    }
}

Initializer

ゲームオブジェクト初期化とイベントリスニングの実装

このコードは、Unityでゲームの初期設定を行うスクリプトです。PlayerControllerオブジェクトにUnityInputProviderを設定し、前方への移動をシミュレートします。また、"TriggerObject"タグが付けられたトリガーオブジェクトを検索し、そのオブジェクトにTriggerEventDetectorコンポーネントのOnEnterイベントにリスナーとしてOnEnterFromEnemyメソッドを登録します。これにより、敵に接触した際の処理をログに記録します。このスクリプトは、ゲームオブジェクトの初期化とイベントベースのインタラクションの設定を行う例を示しています。

using UnityEngine;

public class Initializer : MonoBehaviour
{
    [SerializeField]
    PlayerController playerController;

    void Start()
    {
        // MockInputProviderをセット
        playerController.SetInputProvider(new UnityInputProvider()); // 前方に移動する入力をシミュレート

        // トリガーオブジェクトを取得してイベントリスナーをセット
        var triggerObject = GameObject.FindWithTag("TriggerObject");
        triggerObject.GetComponent<TriggerEventDetector>().OnEnter.AddListener(OnEnterFromEnemy);

    }

    public void OnEnterFromEnemy()
    {
        Debug.Log("エネミーに触れた");
    }
}

PlayerController

入力依存型プレイヤー移動制御システム

このコードはプレイヤーの入力を管理し、キャラクターの移動を実現するためのものです。StartメソッドでCharacterControllerコンポーネントを取得し、SetInputProviderメソッドで入力プロバイダーを設定します。Updateメソッドでは、設定された入力プロバイダーから水平方向と垂直方向の入力値を取得し、それを使ってキャラクターの前方向への移動を計算しています。これにより、さまざまな入力デバイスに対応したプレイヤーの移動制御が可能になります

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    private IInputProvider inputProvider;

    CharacterController characterController;

    void Start()
    {
        characterController = GetComponent<CharacterController>();
    }
    public void SetInputProvider(IInputProvider provider)
    {
        inputProvider = provider;
    }

    void Update()
    {
        if (inputProvider == null) return;

        float moveHorizontal = inputProvider.GetAxis("Horizontal");
        float moveVertical = inputProvider.GetAxis("Vertical");

        // 移動ロジック...
        characterController.Move(Vector3.forward * moveVertical * Time.deltaTime * 10);
    }
}

PreConfiguredSceneTest

非同期シーンロードとイベントトリガーをテストする

このコードは、プレイヤーが動いた時にイベントをトリガーするテストを実装しています。非同期的にシーンをロードし、シーン内のプレイヤーオブジェクトとトリガーオブジェクトを検索します。プレイヤーが前方に移動する入力をシミュレートし、トリガーオブジェクトに接触すると、triggered フラグを真に設定します。このテストは、プレイヤーが期待通りにイベントをトリガーするかを検証します。

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

public class PreConfiguredSceneTest
{
    private GameObject player;
    private PlayerController playerController;
    private bool triggered = false;

    [SetUp]
    public void SetUp()
    {
        // ここに初期化処理を記述する
    }

    [UnityTest]
    public IEnumerator PlayerTriggersEventOnMove()
    {
        // アプリケーションは、現在のシーンが実行されている間、バックグラウンドでシーンをロードします。
        // これは、ローディング画面を作成するのに特に適しています。
        AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("SampleScene");

        // 非同期シーンのロードが完了するまで待ちます
        while (!asyncLoad.isDone)
        {
            yield return null;
        }

        // シーンからプレイヤーオブジェクトを取得
        player = GameObject.FindWithTag("Player");
        Assert.That(player, Is.Not.Null, "プレイヤーオブジェクトがシーン内に見つかりませんでした。");

        // PlayerControllerコンポーネントを取得
        playerController = player.GetComponent<PlayerController>();
        Assert.That(playerController, Is.Not.Null, "プレイヤーにPlayerControllerコンポーネントが見つかりませんでした。");

        // MockInputProviderをセット
        playerController.SetInputProvider(new MockInputProvider(1.0f)); // 前方に移動する入力をシミュレート

        // トリガーオブジェクトを取得してイベントリスナーをセット
        var triggerObject = GameObject.FindWithTag("TriggerObject");
        Assert.That(triggerObject, Is.Not.Null, "トリガーオブジェクトがシーン内に見つかりませんでした。");
        triggerObject.GetComponent<TriggerEventDetector>().OnEnter.AddListener(() => triggered = true);

        // ゲームのフレームを進める
        yield return new WaitForSeconds(0.5f);

        // プレイヤーがトリガーオブジェクトに接触してイベントが発生したか検証
        Assert.That(triggered, Is.True, "プレイヤーが予期されたイベントをトリガーしませんでした。");
    }
}

クラス図

EditModeのテスト

Unity

Posted by hidepon