Unity Test Runner の使い所


1. Edit Mode テストでビジネスロジックを試す

目的: 純粋な C# コード(シーン/ゲームオブジェクト非依存)を高速に検証

  • スコア計算
    • 例: CalculateScore() が期待通りの値を返すか
  • インベントリ操作
    • 例: アイテム追加/削除メソッドの動作確認
  • データ変換・シリアライズ
    • JSON ↔ オブジェクトの整合性チェック

例1: スコア計算機能のテスト(ScoreCalculator)

クラス定義例

public class ScoreCalculator
{
    private readonly int _killPoints;
    private readonly int _timeBonusPerSecond;

    public ScoreCalculator(int killPoints, int timeBonusPerSecond)
    {
        _killPoints = killPoints;
        _timeBonusPerSecond = timeBonusPerSecond;
    }

    /// <summary>
    /// 敵を倒した数とクリア時間から最終スコアを計算する
    /// </summary>
    public int Calculate(int kills, float timeInSeconds)
    {
        int baseScore = kills * _killPoints;
        int timeBonus = Mathf.FloorToInt((60f - timeInSeconds) * _timeBonusPerSecond);
        return Mathf.Max(baseScore + timeBonus, 0);
    }
}

テストクラス例

using NUnit.Framework;
using UnityEngine;  // Mathf を使う場合

[TestFixture]
public class ScoreCalculatorTests
{
    private ScoreCalculator _calculator;

    [SetUp]
    public void SetUp()
    {
        // 1キルあたり100点、1秒早いごとに5点ボーナス
        _calculator = new ScoreCalculator(killPoints: 100, timeBonusPerSecond: 5);
    }

    [Test]
    public void Calculate_GivenZeroKillsAndFullTime_ReturnsZero()
    {
        int result = _calculator.Calculate(kills: 0, timeInSeconds: 60f);
        Assert.That(result, Is.EqualTo(0), "キルもボーナスもない場合は0点になるべき");
    }

    [Test]
    public void Calculate_GivenThreeKillsAndQuickClear_ReturnsCorrectScore()
    {
        // 3キル → 300点、クリア時間50秒 → (60 - 50) * 5 = 50点 → 合計350点
        int result = _calculator.Calculate(kills: 3, timeInSeconds: 50f);
        Assert.That(result, Is.EqualTo(350), "3キルかつ50秒クリアで350点になるべき");
    }

    [Test]
    public void Calculate_NegativeTimeBonus_DoesNotGoBelowZero()
    {
        // クリア時間が60秒以上だと timeBonus がマイナスになるが、最終結果は0未満にならない
        int result = _calculator.Calculate(kills: 0, timeInSeconds: 70f);
        Assert.That(result, Is.EqualTo(0), "マイナスボーナスでもスコアは0未満にならない");
    }
}

例2: インベントリ操作機能のテスト(InventoryManager)

クラス定義例

public class InventoryManager
{
    private readonly List<string> _items = new List<string>();

    public IReadOnlyList<string> Items => _items;

    public void AddItem(string item)
    {
        if (string.IsNullOrEmpty(item))
            throw new ArgumentException("アイテム名は空にできません");
        _items.Add(item);
    }

    public bool RemoveItem(string item)
    {
        return _items.Remove(item);
    }

    public void Clear()
    {
        _items.Clear();
    }
}

テストクラス例

using NUnit.Framework;
using System;

[TestFixture]
public class InventoryManagerTests
{
    private InventoryManager _inventory;

    [SetUp]
    public void SetUp()
    {
        _inventory = new InventoryManager();
    }

    [Test]
    public void AddItem_ValidItem_IncreasesCount()
    {
        _inventory.AddItem("Potion");
        Assert.That(_inventory.Items.Count, Is.EqualTo(1), "アイテム追加後のカウントは1になるべき");
        Assert.That(_inventory.Items[0], Is.EqualTo("Potion"), "追加されたアイテムが正しい");
    }

    [Test]
    public void AddItem_EmptyString_ThrowsArgumentException()
    {
        Assert.That(() => _inventory.AddItem(""), 
                    Throws.ArgumentException.With.Message.Contain("空にできません"));
    }

    [Test]
    public void RemoveItem_ExistingItem_ReturnsTrueAndDecreasesCount()
    {
        _inventory.AddItem("Key");
        bool removed = _inventory.RemoveItem("Key");
        Assert.That(removed, Is.True, "既存アイテムは true を返す");
        Assert.That(_inventory.Items, Does.Not.Contain("Key"), "リストから削除されている");
    }

    [Test]
    public void Clear_OnMultipleItems_EmptiesInventory()
    {
        _inventory.AddItem("Sword");
        _inventory.AddItem("Shield");
        _inventory.Clear();
        Assert.That(_inventory.Items.Count, Is.Zero, "クリア後はアイテム数が0になるべき");
    }
}

これらの Edit Mode テスト例を参考に、

  • 小さなロジック(計算処理や単純データ操作)から始め、
  • セットアップ・検証を明確に分けることで、
  • テストの可読性と 保守性 を高められます。

2. Play Mode テストでゲーム動作を自動化

目的: 実際のシーンやユーザー操作をエミュレートして動作を確認

  • シーンロード検証
    • SceneManager.LoadScene("Main") → オブジェクト配置のチェック
  • 操作シミュレーション
    • キーボード入力やコライダー衝突の自動テスト
  • UI フロー確認
    • メニュー開閉やボタン押下後の画面遷移を実行

例1: シーンロードとプレイヤー生成確認

テスト概要

  • シーン名:`MainScene`  
  • 検証内容:シーン読み込み後に必ず `"Player"` という名前のゲームオブジェクトがシーン上に存在すること

テストコード例

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

public class SceneLoadTests
{
    private const string SceneName = "MainScene";

    [UnityTest]
    public IEnumerator LoadScene_PlayerIsSpawned()
    {
        // シーン読み込みイベントを待機するフラグ
        bool sceneLoaded = false;
        SceneManager.sceneLoaded += (scene, mode) => {
            if (scene.name == SceneName)
                sceneLoaded = true;
        };

        // シーンを非同期読み込み
        var loadOp = SceneManager.LoadSceneAsync(SceneName);
        yield return new WaitUntil(() => sceneLoaded && loadOp.isDone);

        // プレイヤーを検索
        var player = GameObject.Find("Player");
        Assert.IsNotNull(player, "MainScene 読み込み後に Player オブジェクトがシーン上に存在するはず");

        // プレイヤーの初期位置検証(例:ワールド原点付近)
        Vector3 pos = player.transform.position;
        Assert.That(pos, Is.TypeOf<Vector3>(), "Player の位置が Vector3 で取得できること");
        Assert.That(pos.magnitude, Is.LessThan(0.5f), "Player はワールド原点 (0,0,0) 付近にスポーンされるべき");
    }
}

例2: ユーザー入力シミュレーションと移動挙動検証

テスト概要

  • プレイヤーに前進入力を送り、PlayerMovement コンポーネントによる移動が正しく行われることを確認
  • Input.GetAxis(“Vertical") をモックして前進させる

テストコード例

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

// キーボード入力をプログラムから操作するヘルパークラス
public static class InputSimulator
{
    public static void SetVertical(float value)
    {
        // UnityEngine.InputSystem を使う場合はそちらを利用
        // ここでは Movement スクリプト内で参照している変数を直接書き換える想定
        PlayerInputMock.Vertical = value;
    }
}

// モック入力保持用(テスト用に用意しておく)
public static class PlayerInputMock
{
    public static float Vertical;
}

// プレイヤー移動スクリプト例
public class PlayerMovement : MonoBehaviour
{
    public float speed = 5f;
    void Update()
    {
        float v = PlayerInputMock.Vertical;  // 通常は Input.GetAxis("Vertical")
        transform.Translate(Vector3.forward * v * speed * Time.deltaTime);
    }
}

public class PlayerMovementTests
{
    [UnityTest]
    public IEnumerator PlayerMovesForward_WhenInputPositive()
    {
        // テスト用 GameObject とスクリプトを生成
        var go = new GameObject("Player");
        var movement = go.AddComponent<PlayerMovement>();
        movement.speed = 2f;

        // 初期位置を保存
        Vector3 startPos = go.transform.position;

        // 前進入力をシミュレート
        InputSimulator.SetVertical(1f);

        // 一フレームだけ待機して移動させる
        yield return null;  // Update が一度実行される

        Vector3 endPos = go.transform.position;
        Assert.That(endPos.z, Is.GreaterThan(startPos.z), "前進入力で Z 軸正方向に移動するはず");
        Assert.That((endPos - startPos).magnitude, Is.InRange(0.01f, 0.2f), "移動距離が speed * Time.deltaTime 程度であることを検証");
    }
}

例3: UI ボタン押下後の画面遷移テスト

テスト概要

  • メインメニュー画面の「Start」ボタンをクリックし、GameScene へ遷移することを検証

テストコード例テストコード例

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

public class MenuFlowTests
{
    [UnityTest]
    public IEnumerator ClickStartButton_LoadsGameScene()
    {
        // メニューシーンを同期読み込み
        SceneManager.LoadScene("MenuScene");
        yield return null;

        // ボタン取得
        var buttonGO = GameObject.Find("StartButton");
        Assert.IsNotNull(buttonGO, "StartButton がシーン上に存在すること");
        var button = buttonGO.GetComponent<Button>();

        // SceneManager.sceneLoaded で遷移を待機
        bool sceneLoaded = false;
        SceneManager.sceneLoaded += (sc, mode) => {
            if (sc.name == "GameScene") sceneLoaded = true;
        };

        // ボタンのクリックをエミュレート
        button.onClick.Invoke();
        yield return new WaitUntil(() => sceneLoaded);

        Assert.AreEqual("GameScene", SceneManager.GetActiveScene().name, "ボタン押下後 GameScene に遷移すること");
    }
}

これらの詳細例を参考に、

  • シーンの読み込み・イベント待機、
  • フレーム単位での入力/移動シミュレーション、
  • UI イベントのエミュレート

などを組み合わせて、実際のゲーム挙動を自動化テストできます。


3. CI/CD パイプラインへの組み込み

目的: Git コミット/プルリク時に自動でテストを実行し品質を担保

  1. GitHub Actions / Azure Pipelines / Jenkins などを設定
  2. プッシュ時に Edit Mode & Play Mode テストを自動実行
  3. すべてのテスト合格をデプロイ条件に

4. リファクタリング時の安全網として

目的: コードを大きく書き換える前に「壊れてはいけない動作」をテストで定義

  • 既存機能の振る舞い固定
    • リファクタリング後も同じテストが通ることを確認
  • ドキュメント代わりに
    • テスト名・アサーションで仕様を明文化

5. バグ修正後の回帰防止

目的: 発見したバグをテストケース化し、再発を防ぐ

  1. Issue 再現シナリオを UnityTest メソッドに記述
  2. 修正後にテストを通して確認
  3. 将来の変更でも同じバグが発生しないことを保証

おすすめのはじめ方

  1. 小さな機能からテストを書く
    • まずはシンプルな計算メソッドから
  2. Edit Mode/Play Mode を使い分ける
    • シーン依存なし → Edit Mode
    • ゲーム挙動 → Play Mode
  3. テストの独立性を保つ
    • 外部依存(I/O/ネットワーク)はモック化
  4. 継続的に実行する
    • エディタ起動時・CI に組み込んで自動化

これらの使い所を押さえれば、Unity の品質向上と開発効率アップを初心者のうちから体感できます。ぜひ小さく始めて、徐々にカバレッジを広げてみてください!

参考)その他の用途

Edit Mode テストの使い所例

  • ダメージ計算ロジックの検証
    • 例: DamageCalculator.Calculate(int attack, int defense) が期待通りのダメージを返すか
  • 経験値/レベルアップ計算
    • 例: ExperienceCalculator.GetRequiredXP(int currentLevel) が次レベルに必要な経験値を正しく算出するか
  • コンボシーケンス判定
    • 例: ComboValidator.IsValidSequence(List<Input> sequence) が正しいキー入力列をコンボと認識するか
  • クールダウン管理
    • 例: CooldownTimer.IsReady(string skillId) がスキル再使用可能タイミングを正しく判定するか
  • パスファインディングアルゴリズム
    • 例: PathFinder.FindPath(Vector2 start, Vector2 goal) が最短経路を返しているか
  • リーダーボードソート
    • 例: LeaderboardManager.Sort(List<PlayerScore> scores) がスコア降順に正しくソートするか
  • 設定ロード/バリデーション
    • 例: SettingsLoader.Load() が設定ファイル未存在時にデフォルト値を返すか
  • データ暗号化/復号
    • 例: EncryptionService.Encrypt(string data)Decrypt() で元の文字列に戻るか
  • テキストローカライズ
    • 例: Localization.GetText(string key) が指定 locale の翻訳を正しく返すか
  • メタデータ生成
    • 例: MapMetadataGenerator.Generate(int width, int height) がタイル数や障害物配置を含むメタ情報を生成するか

Play Mode テストの使い所例

  • キャラクター移動の検証
    • 例: ジョイスティック入力をシミュレートし、2 秒後のワールド座標が期待値と一致するか
  • コライダー&物理挙動テスト
    • 例: プレイヤーがジャンプして床に着地するまでの落下時間と衝突判定を確認
  • UI ボタン操作と画面遷移
    • 例: “Start” ボタンを押す → ゲームシーンがロードされ、メインカメラが有効化されるか
  • アニメーションステート遷移
    • 例: Enemy の Animator を再生 → Idle → Attack → Die の各ステートに正しく移行するか
  • サウンド再生検証
    • 例: ダメージを受けたときに指定の SoundClip が一度だけ再生されるか
  • パーティクルエフェクトの発生
    • 例: 弾丸が敵にヒットしたときに Explosion の ParticleSystem が起動し、指定時間アクティブか
  • ゲームオーバー/リスタートフロー
    • 例: プレイヤー HP が 0 以下 → GameOver UI が表示され、リスタートボタン押下で最初のシーンに戻るか
  • セーブ&ロード動作
    • 例: プレイ中にセーブ → ゲームを再起動してロード → キャラクターの位置・ステータスが保持されるか
  • カメラフォロー機能
    • 例: プレイヤー移動中にカメラが適切なオフセットで追従し続けるか
  • マルチプレイヤー同期テスト
    • 例: NetworkManager をモックし、別クライアントの位置情報が正しく更新されるか

Test,Unity

Posted by hidepon