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 コミット/プルリク時に自動でテストを実行し品質を担保
- GitHub Actions / Azure Pipelines / Jenkins などを設定
- プッシュ時に Edit Mode & Play Mode テストを自動実行
- すべてのテスト合格をデプロイ条件に
4. リファクタリング時の安全網として
目的: コードを大きく書き換える前に「壊れてはいけない動作」をテストで定義
- 既存機能の振る舞い固定
- リファクタリング後も同じテストが通ることを確認
- ドキュメント代わりに
- テスト名・アサーションで仕様を明文化
5. バグ修正後の回帰防止
目的: 発見したバグをテストケース化し、再発を防ぐ
- Issue 再現シナリオを UnityTest メソッドに記述
- 修正後にテストを通して確認
- 将来の変更でも同じバグが発生しないことを保証
おすすめのはじめ方
- 小さな機能からテストを書く
- まずはシンプルな計算メソッドから
- Edit Mode/Play Mode を使い分ける
- シーン依存なし → Edit Mode
- ゲーム挙動 → Play Mode
- テストの独立性を保つ
- 外部依存(I/O/ネットワーク)はモック化
- 継続的に実行する
- エディタ起動時・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 の各ステートに正しく移行するか
- 例: Enemy の
- サウンド再生検証
- 例: ダメージを受けたときに指定の SoundClip が一度だけ再生されるか
- パーティクルエフェクトの発生
- 例: 弾丸が敵にヒットしたときに Explosion の ParticleSystem が起動し、指定時間アクティブか
- ゲームオーバー/リスタートフロー
- 例: プレイヤー HP が 0 以下 → GameOver UI が表示され、リスタートボタン押下で最初のシーンに戻るか
- セーブ&ロード動作
- 例: プレイ中にセーブ → ゲームを再起動してロード → キャラクターの位置・ステータスが保持されるか
- カメラフォロー機能
- 例: プレイヤー移動中にカメラが適切なオフセットで追従し続けるか
- マルチプレイヤー同期テスト
- 例: NetworkManager をモックし、別クライアントの位置情報が正しく更新されるか
ディスカッション
コメント一覧
まだ、コメントがありません