【Unity】Unity Test Framework入門:テストでバグを未然に防ぐ

作成: 2026-02-05最終更新: 2026-07-13

Debug.Logの目視確認と自動テストは何が違うのか——から始めるUnity Test Framework入門。Edit Mode/Play Modeテストの使い分け、AAAパターン、つまずきやすいAssembly Definitionの正体、ゲームで「何をテストすべきか」の線引きまで解説します。

動作確認環境: Unity 2022.3 LTS / Unity 6

「このコードを変更したら、他の部分が壊れてしまった…」そんな経験、ありませんか? 直したはずのバグが別の場所で再発する——コードが育つほど、手動確認だけでは追いつかなくなります。

Unity Test Framework は、Unity公式のテストフレームワークです。定番のテストライブラリ NUnit をUnityに統合したもので、「このメソッドにこの値を入れたら、この結果になるはず」という確認をコードとして書き残し、ボタン一つで何度でも実行できます。書いたテストは、コードを変更するたびに「壊れていないよ」と教えてくれる見張り番になります。

テストのイメージ。ロボットの検査官がコードブロックを一つずつ検査し、合格のチェックマークを付けている

この記事でわかること

  • いつもの「Debug.Logで動作確認」と自動テストの決定的な違い
  • Edit ModeテストとPlay Modeテストの使い分け
  • AAAパターンでの書き方と、わざと赤にして直す「赤→緑」の練習
  • [UnityTest]によるフレームをまたぐPlay Modeテスト
  • Assembly Definitionの正体と設定方法
  • ゲーム開発で「何をテストすべきか」の現実的な線引き

Sponsored

いつものDebug.Log確認と何が違うのか

普段の開発では、Debug.Logで値を出して、再生して、目で見て「OK、動いてる」——で済ませている人がほとんどだと思います(デバッグ手法入門 のやり方です)。それで開発は進むのに、なぜわざわざテストを書くのでしょうか。

違いは1つだけです。Debug.Logの確認は「その場で消える」、テストは「確認が保存される」

Debug.Log確認とテストの比較図。Debug.Logは1回目視で確認したら消えてしまい、後日の変更で壊れても誰も気づかない。テストは確認がコードとして保存され、変更のたびに自動で再実行されて壊れた瞬間に知らせる
  • Debug.Log確認: 「今この瞬間、自分の目で見て正しい」ことを1回だけ確認する。確認が終わればログは消し、確認したという事実も消える
  • テスト: 「この入力ならこの結果になるはず」という確認をコードとして保存する。以後、ボタン一つで何百回でも機械が再確認してくれる

これが効くのは後日です。例えばRPGのダメージ計算に会心仕様を追加したとき、うっかり「防御力が高い敵に0ダメージ」という退行バグを入れてしまったとします。Debug.Log方式では、3週間後にプレイヤーの報告で気づくかもしれません。ダメージ計算のテストを5本保存してあれば、コードを保存したその場で「防御力100のケースが失敗しています」と教えてくれます。

つまりテストとは、過去の自分がやった動作確認を、未来の自分のために自動化して残しておく仕組みです。

テストのメリット

テストが見張り番として働く図。コードを変更した瞬間、テストの盾が「ここが壊れた」とバグを検知して知らせてくれる
  • バグの早期発見 - コード変更後、すぐにバグを発見できる
  • リファクタリングの安全性向上 - 既存機能が壊れていないか確認できる
  • コードの品質向上 - テストしやすいコードは良い設計のコード
  • ドキュメントとしての役割 - コードの使い方を示す

特に効いてくるのが2つ目です。テストがあると「壊したらすぐ分かる」という安心感が生まれ、コードの整理や改善に思い切って手を出せるようになります。テストのない大規模コードの改修が怖いのは、この安全網がないからです。

ゲームのどこをテストする?

「ゲームのテスト」と聞くと途方もなく感じますが、実際にテストを書く場所はかなり限定的です。 答えの決まっている「計算」 が最優先のテスト対象です。

ゲームジャンル別のテスト対象の図。RPGのダメージ計算式、アーケードゲームのスコア・コンボ倍率計算、アドベンチャーのインベントリ増減とセーブデータ変換という3ジャンルの実例
  • RPGのダメージ計算: 「攻撃力50・防御力30・会心なら結果はいくつ?」——式が複雑になるほど、退行バグの温床になります
  • アーケードゲームのスコア計算: コンボ倍率・ボーナス条件の組み合わせは、手動で全パターン確認するのがまず不可能です
  • アイテムのインベントリ処理: 「満杯のときに拾ったら?」「99個上限を超えたら?」という境界のバグは、テストが最も得意とする獲物です

逆に「ジャンプの気持ちよさ」「敵AIの手強さ」のような手触りはテストできません。この線引きは記事末尾のおまけで詳しく扱います。

Sponsored

Edit ModeテストとPlay Modeテストの違い

Edit ModeテストとPlay Modeテストの比較図。Edit Modeはエディタ内でロジックだけを高速に検査、Play Modeは実際にゲームを再生して物理や時間経過を含めて検査
項目Edit ModeテストPlay Modeテスト
実行環境Unity EditorのみEditorまたはPlayer
実行速度高速低速
用途ロジックのテストゲームプレイのテスト
ライフサイクルEditorApplication.updateAwake, Start, Update
アクセス可能Editorコード + ゲームコードEditor実行時はEditorコードも可、Player実行時はゲームコードのみ

インストール

注意: Unity 2019.2以降では、Test Frameworkはデフォルトでインストールされています。Package Managerでの手動インストールは不要です。

Test RunnerウィンドウはWindow > General > Test Runnerで開きます。

そもそもAssembly Definitionとは

テスト導入で最初につまずくのが、この Assembly Definition(.asmdef) という耳慣れない仕組みです。正体を知れば単純です。

Unityは通常、プロジェクト内の全スクリプトをまとめて1冊の本Assembly-CSharpというアセンブリ=コンパイル済みコードの束)に綴じ込みます。ところがテストコードはゲームのビルドに混ぜたくないため、「テスト用の別の本」として分冊する必要があります。この分冊の単位を定義するのが.asmdefファイルです。

Assembly Definitionの図。ゲーム本体のコードとテストコードが別々の本(アセンブリ)に分かれていて、テストの本から本体�の本へ「参照」の矢印が伸びている。ビルドにはテストの本が含まれない

そして分冊すると、別の本からは本体の中身が見えなくなります。テストコードからPlayerControllerDamageCalculatorを呼ぼうとしてもコンパイルエラーになるのはこのためです。そこで「テストの本は、本体の本を参照します」と宣言する——これがAssembly Definitionの設定の正体です。

tips: Test Runnerの「Create Test Assembly Folder」ボタンで作ったフォルダには、テスト用.asmdefが自動生成されます。自分で設定するのは「本体への参照の追加」だけです。なお本体側も.asmdef(例: MyGame.Runtime)で分冊されている必要があります(Assembly-CSharpのままだと参照先として選べません)。

Inspectorでの設定手順

  1. Tests.asmdefファイルを選択
  2. Assembly Definition References セクションを開く
  3. +ボタンをクリック
  4. 参照したい本番コードのasmdef(例:MyGame.Runtime)を選択
  5. Apply をクリック

asmdefのJSON例

Edit Modeテスト用:

{
    "name": "Tests.EditMode",
    "references": ["MyGame.Runtime"],
    "includePlatforms": ["Editor"],
    "defineConstraints": ["UNITY_INCLUDE_TESTS"]
}

Play Modeテスト用:

{
    "name": "Tests.PlayMode",
    "references": ["MyGame.Runtime"],
    "includePlatforms": [],
    "defineConstraints": ["UNITY_INCLUDE_TESTS"]
}

重要: Play Modeテストでは"includePlatforms": [](空配列)にする必要があります。["Editor"]のままだとビルドに含まれず、Player上でテストが実行できません。

Sponsored

はじめてのテスト

Test Assemblyの作成

  1. Test RunnerウィンドウでEdit Modeタブを選択
  2. Create EditMode Test Assembly Folderボタンをクリック

テスト対象のゲームコードを用意する

まず「テストされる側」を見てみましょう。冒頭から例に挙げている、RPGのダメージ計算クラスです。MonoBehaviourを継承しない普通のC#クラスなので、Edit Modeテストで一瞬で検証できます。

// ゲーム本体側のコード(テストされる側)
public class DamageCalculator
{
    // ダメージ = 攻撃力 - 防御力。ただし最低1は保証する
    public int Calculate(int attack, int defense)
    {
        int damage = attack - defense;
        return damage < 1 ? 1 : damage;
    }
}

テストスクリプトの作成

このクラスをテストします。テストとは要するに、テストコードがゲームのクラスを実際に呼び出して、返ってきた答えを「期待した答え」と照合するコードです。

テストコードとゲームコードの関係図。テストコードがDamageCalculatorに攻撃力50・防御力30を渡して呼び出し、返ってきた20を期待値20と答え合わせして合格のチェックが付く

書き方は AAAパターン ——Arrange(準備)→ Act(実行)→ Assert(検証)の3ステップが定番です。「材料を並べ、調理し、味見する」の流れです。

AAAパターンの図。Arrangeで材料(テストデータ)を準備し、Actでメソッドを実行し、Assertで結果が期待どおりかを検証する3ステップ
using NUnit.Framework;

public class DamageCalculatorTest
{
    [Test]
    public void Calculate_Attack50_Defense30_Returns20()
    {
        // Arrange(準備): 計算機と入力値を用意する
        var calculator = new DamageCalculator();
        int attack = 50;
        int defense = 30;

        // Act(実行): 実際にゲームのメソッドを呼ぶ
        int damage = calculator.Calculate(attack, defense);

        // Assert(検証): 「50 - 30 = 20 のはず」と答え合わせ
        Assert.AreEqual(20, damage);
    }

    [Test]
    public void Calculate_DefenseHigherThanAttack_ReturnsAtLeast1()
    {
        // 冒頭の退行バグ「防御力が高い敵に0ダメージ」を捕まえるテスト
        var calculator = new DamageCalculator();

        int damage = calculator.Calculate(attack: 30, defense: 100);

        // どんなに防御が高くても、最低1ダメージは出るはず
        Assert.AreEqual(1, damage);
    }
}

Test Runnerで実行すると、この2つに緑のチェックが付きます。そして将来、誰かが「最低1保証」の行をうっかり消したら——2つ目のテストがその瞬間に赤くなって知らせてくれます。これが「確認の保存」の正体です。

わざと赤にしてみる——テストの本当の練習

緑のチェックを見ただけでは、テストの練習は半分です。本当に身につけたいのは 赤くなったテストの読み方 なので、安全ないまのうちに体験しておきましょう。「将来誰かがうっかり消したら」を、いま自分の手で起こしてみます。

わざと赤にして直す練習の3ステップ。最低1保証の行を壊すと、Test Runnerに赤い✗とExpected 1 / But was -70のエラー文が出て、それを読んで直すと緑のチェックに戻る
public int Calculate(int attack, int defense)
{
    int damage = attack - defense;
    return damage;  // わざと壊す(元: return damage < 1 ? 1 : damage;)
}

Test Runnerを再実行すると、2つ目のテストに赤い✗が付き、クリックするとこう表示されます。

Calculate_DefenseHigherThanAttack_ReturnsAtLeast1
  Expected: 1
  But was:  -70

読み方はそのままです——「1のはず(Expected)なのに、実際は-70だった(But was)」。どのテストが・何を期待して・実際はどうだったかが数行で分かるので、闇雲にログを仕込むより先に、まずこのエラー文を読むのが近道です。行をもとに戻して再実行すれば、緑に戻ります。

この「わざと壊す→赤を読む→直して緑」の一周を自分の手で回せたら、テストはもう道具になっています。ついでに、同じテストを何度か連続実行して毎回同じ結果になることも見ておいてください。この「揺れないこと」が見張り番の信頼の土台で、揺れる場合の犯人は後述の不安定なテストで捕まえられます。

テストの書き方

アサーション

Assert(検証)には、答え合わせの形に応じたバリエーションがあります。ゲームでの使いどころとセットで見ると迷いません。

// 等価性: スコア加算の結果は300のはず
Assert.AreEqual(300, scoreManager.TotalScore);

// 真偽値: 満杯のインベントリにはもう追加できないはず
Assert.IsFalse(inventory.Add(potion));

// 真偽値: レベル10でスキルが解放されているはず
Assert.IsTrue(player.HasSkill("FireBall"));

// null: 存在しないアイテムIDの検索はnullを返すはず
Assert.IsNull(itemDatabase.Find("no_such_item"));

// 例外: レベルにマイナス値を渡したら例外を投げるはず
Assert.Throws<System.ArgumentException>(() => player.SetLevel(-1));

// Constraint Model記法(NUnit 3系推奨)
Assert.That(inventory.Count, Is.EqualTo(3));          // クエスト報酬でアイテムが3つ増えたはず
Assert.That(damage, Is.InRange(90, 110));             // 乱数幅(±10%)のあるダメージが範囲内のはず

Constraint Model: NUnit 3系ではAssert.Thatを使った記法が推奨されています。より読みやすく、「乱数で揺れる値が範囲内か」のような複雑な条件も表現できます。

複数の入力パターンを一気にテストする(TestCase)

ダメージ計算のような「入力と期待値の表」で表せるテストは、[TestCase]で1つのメソッドにまとめられます。ケースを追加したくなったら行を1本足すだけです。

// 攻撃力, 防御力, 期待ダメージ
[TestCase(50, 30, 20)]   // 通常: 50 - 30 = 20
[TestCase(30, 100, 1)]   // 防御が上回っても最低1(冒頭の退行バグを常時見張る)
[TestCase(10, 0, 10)]    // 防御0ならそのまま通る
[TestCase(1, 1, 1)]      // ぎりぎりの境界値
public void Calculate_DamageTable(int attack, int defense, int expected)
{
    var calculator = new DamageCalculator();

    int damage = calculator.Calculate(attack, defense);

    Assert.AreEqual(expected, damage);
}

前後処理(SetUp / TearDown)

インベントリのテストを何本も書くとき、毎回「新品のインベントリ」から始めたいはずです。前のテストで追加したアイテムが残っていると、結果が汚染されるからです。この「毎回の準備・後片付け」を自動化するのが前後処理です。

public class InventoryTest
{
    private Inventory inventory;

    [SetUp]
    public void SetUp()
    {
        // 各テストの前に毎回実行: 新品のインベントリ(容量10)を用意
        inventory = new Inventory(capacity: 10);
    }

    [TearDown]
    public void TearDown()
    {
        // 各テストの後に毎回実行: 後片付けが必要ならここで
    }

    [Test]
    public void Add_NewItem_CountIncreases()
    {
        inventory.Add(new Item("Potion"));
        Assert.AreEqual(1, inventory.Count);  // 新品に1つ足したので必ず1
    }

    [Test]
    public void Add_WhenFull_ReturnsFalse()
    {
        // このテストも「新品」から始まるので、前のテストの影響を受けない
        for (int i = 0; i < 10; i++)
        {
            inventory.Add(new Item("Potion"));
        }

        Assert.IsFalse(inventory.Add(new Item("Elixir")));  // 満杯なら拒否されるはず
    }
}

このほか、[OneTimeSetUp][OneTimeTearDown]を使うと「すべてのテストの前後に1回だけ」の処理(重いデータの読み込みなど)が書けます。

補足(テストクラスの属性): テストクラスには[TestFixture]属性を付ける流儀もありますが、通常は省略可能です(パラメータ付き・ジェネリックのテストクラスでのみ必須)。

実開発のテストをシミュレーションしてみる

ここまでの例は1メソッドの単純な計算でした。では実開発では、どのくらいの複雑さのテストを書くことになるのでしょうか。ポイントは、 「仕様の箇条書きが、そのままテストの一覧になる」 ということです。2つのジャンルでシミュレーションしてみます。

例1:RPGの「装備・バフ込みステータス計算」

装備とバフが絡んだ最終攻撃力の計算です。仕様を日本語で書き出すと、こうなります。

  • 最終攻撃力 =(基礎攻撃力 + 装備補正の合計)× バフ倍率
  • 同じバフを重ねがけしても、効果は1回分だけ
  • 最終値は 1〜999 の範囲に収める

この仕様の1行1行が、そのままテストになります。

RPGステータス計算の仕様がテスト一覧に変換される図。「装備補正は合算」「同じバフは1回分」「999を超えない」という仕様カードが、それぞれ対応するテスト項目のチェックリストへ変換される
public class CharacterStatsTest
{
    private CharacterStats stats;

    [SetUp]
    public void SetUp()
    {
        // 毎回「素の状態のキャラ」(基礎攻撃力100・装備なし・バフなし)から始める
        stats = new CharacterStats(baseAttack: 100);
    }

    [Test]
    public void FinalAttack_NoEquipNoBuff_ReturnsBase()
    {
        Assert.AreEqual(100, stats.FinalAttack);  // 素の状態なら基礎値そのまま
    }

    [Test]
    public void FinalAttack_TwoWeapons_AddsBothBonuses()
    {
        stats.Equip(new Item("剣", attackBonus: 30));
        stats.Equip(new Item("指輪", attackBonus: 20));

        Assert.AreEqual(150, stats.FinalAttack);  // 100 + 30 + 20
    }

    [Test]
    public void FinalAttack_SameBuffTwice_AppliesOnlyOnce()
    {
        stats.AddBuff("攻撃アップ", multiplier: 1.5f);
        stats.AddBuff("攻撃アップ", multiplier: 1.5f);  // 重ねがけ!

        Assert.AreEqual(150, stats.FinalAttack);  // 100 × 1.5(×2.25になったらバグ)
    }

    [Test]
    public void FinalAttack_ExtremeBuff_IsClampedTo999()
    {
        stats.Equip(new Item("伝説の剣", attackBonus: 900));
        stats.AddBuff("攻撃アップ", multiplier: 10f);

        Assert.AreEqual(999, stats.FinalAttack);  // 上限クランプが効くはず
    }
}

注目してほしいのは3本目です。「同じバフの重ねがけで効果が2乗になる」は、RPG開発で実際に頻発するバグです。そして装備・バフ・上限が絡む計算は、手動確認では組み合わせを網羅しきれません。仕様が決まった時点でこのテスト群を書いておけば、後からバフの仕様を拡張しても安心して触れます。

例2:パズルゲームの「マッチ判定」

マッチ3パズル(同色3つ揃いで消えるパズル)の判定ロジックです。実開発らしいポイントは、Arrange(盤面の準備)が大きくなること。毎回セルを1個ずつ置くのは大変なので、盤面を文字列で組み立てるヘルパーを作るのが定番テクニックです。

パズルのマッチ判定テストの図。文字列で組み立てた小さな盤面グリッドで、横3つ揃いが光って検出され、2つ並びは検出されず、L字型は1つのマッチとして検出される3ケース
public class MatchFinderTest
{
    // 文字列から盤面を組み立てるヘルパー(R=赤, B=青, .=空)
    // Arrangeが複雑になったら、こうした補助メソッドに切り出すのが実開発の定番
    private Board CreateBoard(params string[] rows)
    {
        return Board.FromText(rows);
    }

    [Test]
    public void Find_ThreeInARow_DetectsMatch()
    {
        var board = CreateBoard(
            "R R R B",
            "B B R B",
            ". B B R");

        var matches = new MatchFinder().Find(board);

        Assert.AreEqual(1, matches.Count);  // 上段の横RRRが1件検出されるはず
    }

    [Test]
    public void Find_OnlyTwoInARow_NoMatch()
    {
        var board = CreateBoard(
            "R R B B",
            "B R B R");

        var matches = new MatchFinder().Find(board);

        Assert.AreEqual(0, matches.Count);  // 2つ並びはマッチではない
    }

    [Test]
    public void Find_LShape_DetectsAsSingleMatch()
    {
        var board = CreateBoard(
            "R B B",
            "R B .",
            "R R R");

        var matches = new MatchFinder().Find(board);

        Assert.AreEqual(1, matches.Count);  // 縦横が交差するL字は「1つのマッチ」の仕様
    }
}

「L字は1マッチか2マッチか」のような仕様の曖昧な部分こそ、テストを書く過程であぶり出されます。テストを書く=仕様を1つずつ確定させる作業でもある、というのが実開発での実感です。

実開発のテストに共通すること

  • 仕様の箇条書き → 1行ずつテスト名にする。テスト一覧がそのまま「仕様書」として機能する
  • Arrangeが肥大化したらヘルパーに切り出す(盤面ビルダー、キャラ生成ファクトリなど)。テストコードも本体と同じくリファクタリングの対象
  • バグ報告が来たら、まず「そのバグを再現するテスト」を書いてから直す。直った瞬間にテストが緑になり、再発も永久に見張ってくれる

Play Mode用の前後処理(IEnumerator版)

public class MyPlayModeTest
{
    [UnitySetUp]
    public IEnumerator SetUp()
    {
        // 非同期のセットアップ処理
        yield return null;
    }

    [UnityTearDown]
    public IEnumerator TearDown()
    {
        // 非同期のクリーンアップ処理
        yield return null;
    }
}
Sponsored

Play Modeテスト

ここまでのダメージ計算やインベントリは「純粋な計算」なのでEdit Modeで一瞬でテストできました。しかし「Rigidbodyを付けたオブジェクトはちゃんと落下するか」のような、物理・時間経過・GameObjectが絡む確認は、実際にゲームを再生しないと検証できません。それを自動化するのがPlay Modeテストです。

UnityTest属性

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

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 GameObject_WithRigidbody_FallsDown()
    {
        // Arrange
        testObject.AddComponent<Rigidbody>();
        var initialPosition = testObject.transform.position;

        // Act - 条件が満たされるまで待機(タイムアウト付き)
        float timeout = 3f;
        float elapsed = 0f;
        while (testObject.transform.position.y >= initialPosition.y && elapsed < timeout)
        {
            elapsed += Time.deltaTime;
            yield return null;
        }

        // Assert
        Assert.Less(elapsed, timeout, "Timed out waiting for object to fall");
        Assert.Less(testObject.transform.position.y, initialPosition.y);
    }
}

// 待機パターンの比較
// yield return new WaitForSeconds(1.0f);        // CI/CDで遅延の原因になりがち
// yield return new WaitForFixedUpdate();        // 物理演算1ステップ(高速)
// for (int i = 0; i < 60; i++) yield return null;  // 特定フレーム数(約1秒@60fps)

テストのカテゴリ分け

[Category("Combat")]
[Test]
public void Damage_AppliedCorrectly() { }

[Category("Inventory")]
[Test]
public void Item_AddedToInventory() { }

// コマンドラインでカテゴリ指定実行:
// Unity.exe -batchmode -runTests -testCategory Combat -projectPath /path/to/project

async/awaitテスト(Unity 2023.1以降)

// Unity 2023.1以降で使用可能
[Test]
public async Task AsyncOperation_Completes()
{
    var result = await SomeAsyncMethod();
    Assert.IsNotNull(result);
}

よくある問題と解決策

テストが認識されない

  • Assembly Definitionが正しく設定されているか確認
  • テストメソッドに[Test]または[UnityTest]属性が付いているか確認
  • テストクラスがpublicであるか確認

エラーログでテストが失敗する

LogAssert.Expect()を使用してエラーログを期待する:

// 正しい: Expectを先に呼ぶ
LogAssert.Expect(LogType.Error, "Error message");
Debug.LogError("Error message");  // この後に発生するログを期待

// 間違い: ログが先に出力されるとテストが失敗する
// Debug.LogError("Error message");
// LogAssert.Expect(LogType.Error, "Error message");  // 手遅れ

重要: LogAssert.Expect()は必ずログ出力の に呼び出してください。順序を間違えるとテストが失敗します。

意図的にエラーを発生させるテストの場合:

[SetUp]
public void SetUp()
{
    // テスト中のエラーログによるテスト失敗を無視
    LogAssert.ignoreFailingMessages = true;
}

[TearDown]
public void TearDown()
{
    LogAssert.ignoreFailingMessages = false;  // 必ず戻す
}

たまに赤くなる(不安定なテスト)

「実行するたびに結果が違う」テストは、ゲームのバグではなく テスト自体のバグ です。放置すると「また誤報か」とテスト全体が信用されなくなるので、症状から犯人を特定して直します。

症状犯人直し方
CIや遅いPCでだけ落ちるWaitForSecondsなど実時間の待機「条件を満たすまで待つ+タイムアウト」方式へ(本文の落下テスト参照)
10回に1回くらい落ちる乱数を固定していないテスト冒頭でRandom.InitState(12345)のようにシードを固定する。乱数幅ごと検証するならIs.InRange
単体では通るのに全実行だと落ちる前のテストの残留(staticな値・シーンに残ったオブジェクト)SetUpで毎回初期化し、TearDownで確実に片付ける。staticやSingletonは残留しやすいと知っておく

見張り番として信頼できる最低ラインは、同じテストを10回連続で実行して、10回とも同じ結果になることです。

ベストプラクティス

  • テストは小さく保つ - 1つのテストで1つのことだけをテスト
  • テスト名は分かりやすく - メソッド名_条件_期待結果のパターン
  • AAAパターンに従う - Arrange、Act、Assert
  • テストは独立させる - 他のテストに依存しない
  • TearDownでクリーンアップ - テスト失敗時もリソースを確実に解放
  • テストも定期的にリファクタリングする

CI/CDでのテスト実行

コマンドラインからテストを実行できます。

# Windows
Unity.exe -batchmode -runTests -testPlatform EditMode -projectPath /path/to/project

# macOS
/Applications/Unity/Hub/Editor/{version}/Unity.app/Contents/MacOS/Unity -batchmode -runTests -testPlatform EditMode -projectPath /path/to/project

# 結果をXMLで出力
Unity.exe -batchmode -runTests -testResults ./results.xml -projectPath /path/to/project

CI/CD統合: GitHub Actions、Jenkins、GitLab CIなどで自動テストを実行し、プルリクエストごとにテストを実行することで品質を維持できます。{version}は使用するUnityのバージョン(例:2022.3.10f1)に置き換えてください。

テストカバレッジの確認

Code Coverage パッケージ(com.unity.testtools.codecoverage)をインストールすると、テストがどの程度コードを網羅しているか可視化できます。

  1. Package ManagerでCode Coverageをインストール
  2. Window > Analysis > Code Coverageで確認
  3. テスト実行後にレポートを生成

おまけ:先に知っておくと良いこと

テストの書き方が分かったら、次は「何を書くか」です。ゲーム開発ならではの現実的な線引きを知っておきましょう。

  • 全部はテストしない——「計算」から始める: 「ジャンプが気持ちいいか」はテストできませんが、「ダメージ計算が正しいか」はテストできます。まずは Unityに依存しない純粋なロジック(ダメージ計算、スコア集計、インベントリの増減、セーブデータの変換)から始めるのが費用対効果の最も高い一歩です。
  • テストしやすさは設計で決まる: MonoBehaviourUpdateに埋まったロジックはテストできません。計算部分を普通のC#クラスに切り出すとEdit Modeテストで一瞬で検証できます。これは イベントとデリゲートの記事 で扱った疎結合の考え方と地続きです。
  • async/awaitのテスト: Unity 2023.1以降はasync Taskのテストが書けます(本文参照)。async自体の使いどころは コルーチン vs async/await の記事 で整理しています。

判断基準は1行です。 「そのコードは、壊れたことに自分ではすぐ気づけないか」 ——気づきにくい計算・データ処理こそ、テストという見張り番を置く価値があります。

まとめ

Unity Test Frameworkは、Unityプロジェクトの品質を向上させるための強力なツールです。

  • Edit Modeテスト - ロジックのテストに最適、高速実行
  • Play Modeテスト - ゲームプレイのテストに最適
  • AAAパターン - Arrange、Act、Assertで構造化
  • 前後処理 - SetUp、TearDownで共通処理を定義
  • TestCase属性 - 複数の入力値で同じテストを実行

プロジェクトの規模が大きくなるほど、テストの重要性は高まります。まずは自分のゲームの「答えが決まっている計算」を1つ選んで、テストを2本——そして、わざと赤にする練習を1回。今日の開発の締めに試してみませんか?