Unity Test Frameworkとは?

Unity Test Frameworkは、Unityに標準搭載されているテストフレームワークです。

NUnitをベースとしており、C#のユニットテストを作成・実行できます。コードの品質を保ち、リファクタリングやバグ修正を安全に行うために、テストは非常に重要な役割を果たします。

Unity Test Frameworkには、EditModeテストとPlayModeテストの2種類があり、それぞれ異なる目的で使用します。

EditModeテストとは?

EditModeテストは、Unity Editorの環境で実行されるユニットテストです。

ゲームの実行を伴わず、エディタ上で即座にテストが完了します。

計算ロジック、ユーティリティクラス、データクラスなど、GameObjectMonoBehaviourのライフサイクルに依存しないコードのテストに最適です。

PlayModeテストとの違い

特徴EditModePlayMode
実行環境エディタ環境ゲーム実行環境
実行速度高速低速
GameObject使用制限あり可能
用途ロジックのテストゲーム動作のテスト

EditModeテストは高速に実行できるため、開発中に頻繁に実行して、コードの変更が既存機能を壊していないか確認できます。

導入方法

Test Frameworkパッケージのインストール

Unity Test Frameworkは、Unity 2019.2以降では標準でインストールされています。

もしインストールされていない場合は、Package Managerから追加できます。

  1. WindowPackage Managerを開く
  2. Unity RegistryからTest Frameworkを検索
  3. インストール

Test Runnerウィンドウの開き方

テストを実行・管理するには、Test Runnerウィンドウを使用します。

WindowGeneralTest Runnerでウィンドウを開きます。

テスト用フォルダの作成

テストコードは専用のフォルダに配置します。

  1. Projectウィンドウで右クリック
  2. CreateTestingTests Assembly Folderを選択
  3. フォルダ名をTestsなどに設定

この操作により、テスト専用のAssembly Definitionを持つフォルダが作成されます。

基本的なテストの書き方

最もシンプルなテストの例を見てみましょう。

シンプルな計算テスト

using NUnit.Framework;

public class SimpleCalculatorTest
{
    [Test]
    public void Add_TwoPlusThree_ReturnsFive()
    {
        // Arrange(準備)
        int a = 2;
        int b = 3;

        // Act(実行)
        int result = a + b;

        // Assert(検証)
        Assert.AreEqual(5, result);
    }

    [Test]
    public void Multiply_TwoTimesThree_ReturnsSix()
    {
        // Arrange
        int a = 2;
        int b = 3;

        // Act
        int result = a * b;

        // Assert
        Assert.AreEqual(6, result);
    }
}

テストの構造:

  • [Test]属性: メソッドがテストであることを示す
  • Arrange(準備): テストに必要なデータを準備
  • Act(実行): テスト対象のコードを実行
  • Assert(検証): 結果が期待通りか検証

テスト命名規則

テストメソッド名は、以下の形式が推奨されます:

メソッド名_テスト条件_期待される結果

例:

  • Add_TwoPlusThree_ReturnsFive
  • GetDamage_WithArmor_ReducesDamage
  • IsValid_WithNullParameter_ReturnsFalse

EditModeテストの実装例

計算ロジックのテスト

ダメージ計算のユーティリティクラスをテストします。

テスト対象のクラス:

public static class DamageCalculator
{
    public static int CalculateDamage(int baseDamage, int defense)
    {
        if (baseDamage < 0 || defense < 0)
        {
            throw new System.ArgumentException("ダメージと防御力は0以上である必要があります");
        }

        int damage = baseDamage - defense;
        return damage > 0 ? damage : 0;
    }

    public static float CalculateCriticalDamage(int baseDamage, float criticalMultiplier)
    {
        return baseDamage * criticalMultiplier;
    }
}

テストコード:

using NUnit.Framework;
using System;

public class DamageCalculatorTest
{
    [Test]
    public void CalculateDamage_WithNormalValues_ReturnsCorrectDamage()
    {
        // Arrange
        int baseDamage = 100;
        int defense = 30;

        // Act
        int result = DamageCalculator.CalculateDamage(baseDamage, defense);

        // Assert
        Assert.AreEqual(70, result);
    }

    [Test]
    public void CalculateDamage_WhenDefenseExceedsDamage_ReturnsZero()
    {
        // Arrange
        int baseDamage = 50;
        int defense = 100;

        // Act
        int result = DamageCalculator.CalculateDamage(baseDamage, defense);

        // Assert
        Assert.AreEqual(0, result);
    }

    [Test]
    public void CalculateDamage_WithNegativeBaseDamage_ThrowsException()
    {
        // Arrange
        int baseDamage = -10;
        int defense = 30;

        // Act & Assert
        Assert.Throws<ArgumentException>(() =>
        {
            DamageCalculator.CalculateDamage(baseDamage, defense);
        });
    }

    [Test]
    public void CalculateCriticalDamage_WithDoubleMultiplier_ReturnsDoubledDamage()
    {
        // Arrange
        int baseDamage = 100;
        float criticalMultiplier = 2.0f;

        // Act
        float result = DamageCalculator.CalculateCriticalDamage(baseDamage, criticalMultiplier);

        // Assert
        Assert.AreEqual(200f, result);
    }
}

データクラスのテスト

ScriptableObjectを使ったデータクラスのテストです。

テスト対象のクラス:

using UnityEngine;

[CreateAssetMenu(fileName = "EnemyData", menuName = "Game/Enemy Data")]
public class EnemyData : ScriptableObject
{
    public string enemyName;
    public int maxHp;
    public int attackPower;

    public bool IsValid()
    {
        return !string.IsNullOrEmpty(enemyName) && maxHp > 0 && attackPower >= 0;
    }

    public int GetDamageWithMultiplier(float multiplier)
    {
        return Mathf.RoundToInt(attackPower * multiplier);
    }
}

テストコード:

using NUnit.Framework;
using UnityEngine;

public class EnemyDataTest
{
    private EnemyData _enemyData;

    [SetUp]
    public void SetUp()
    {
        // 各テストの実行前に呼ばれる
        _enemyData = ScriptableObject.CreateInstance<EnemyData>();
    }

    [TearDown]
    public void TearDown()
    {
        // 各テストの実行後に呼ばれる
        Object.DestroyImmediate(_enemyData);
    }

    [Test]
    public void IsValid_WithValidData_ReturnsTrue()
    {
        // Arrange
        _enemyData.enemyName = "Goblin";
        _enemyData.maxHp = 100;
        _enemyData.attackPower = 20;

        // Act
        bool result = _enemyData.IsValid();

        // Assert
        Assert.IsTrue(result);
    }

    [Test]
    public void IsValid_WithEmptyName_ReturnsFalse()
    {
        // Arrange
        _enemyData.enemyName = "";
        _enemyData.maxHp = 100;
        _enemyData.attackPower = 20;

        // Act
        bool result = _enemyData.IsValid();

        // Assert
        Assert.IsFalse(result);
    }

    [Test]
    public void IsValid_WithZeroHp_ReturnsFalse()
    {
        // Arrange
        _enemyData.enemyName = "Goblin";
        _enemyData.maxHp = 0;
        _enemyData.attackPower = 20;

        // Act
        bool result = _enemyData.IsValid();

        // Assert
        Assert.IsFalse(result);
    }

    [Test]
    public void GetDamageWithMultiplier_WithTwoTimesMultiplier_ReturnsDoubledDamage()
    {
        // Arrange
        _enemyData.attackPower = 25;
        float multiplier = 2.0f;

        // Act
        int result = _enemyData.GetDamageWithMultiplier(multiplier);

        // Assert
        Assert.AreEqual(50, result);
    }
}

文字列処理のテスト

ユーティリティクラスの文字列処理をテストします。

テスト対象のクラス:

public static class StringHelper
{
    public static string Truncate(string text, int maxLength)
    {
        if (string.IsNullOrEmpty(text))
        {
            return text;
        }

        if (text.Length <= maxLength)
        {
            return text;
        }

        return text.Substring(0, maxLength) + "...";
    }

    public static string RemoveWhiteSpace(string text)
    {
        return text?.Replace(" ", "").Replace("\\t", "").Replace("\\n", "");
    }
}

テストコード:

using NUnit.Framework;

public class StringHelperTest
{
    [Test]
    public void Truncate_WithShortText_ReturnsOriginalText()
    {
        // Arrange
        string text = "Hello";
        int maxLength = 10;

        // Act
        string result = StringHelper.Truncate(text, maxLength);

        // Assert
        Assert.AreEqual("Hello", result);
    }

    [Test]
    public void Truncate_WithLongText_ReturnsTruncatedText()
    {
        // Arrange
        string text = "This is a very long text";
        int maxLength = 10;

        // Act
        string result = StringHelper.Truncate(text, maxLength);

        // Assert
        Assert.AreEqual("This is a ...", result);
    }

    [Test]
    public void Truncate_WithNullText_ReturnsNull()
    {
        // Arrange
        string text = null;
        int maxLength = 10;

        // Act
        string result = StringHelper.Truncate(text, maxLength);

        // Assert
        Assert.IsNull(result);
    }

    [Test]
    public void RemoveWhiteSpace_WithSpaces_ReturnsTextWithoutSpaces()
    {
        // Arrange
        string text = "Hello World Test";

        // Act
        string result = StringHelper.RemoveWhiteSpace(text);

        // Assert
        Assert.AreEqual("HelloWorldTest", result);
    }
}

よく使うAssertメソッド

Assertクラスには、様々な検証メソッドが用意されています。

等価性の検証

// 値が等しいか
Assert.AreEqual(expected, actual);

// 値が等しくないか
Assert.AreNotEqual(unexpected, actual);

// 浮動小数点の比較(誤差を許容)
Assert.AreEqual(1.0f, 1.001f, 0.01f);

真偽値の検証

// trueか
Assert.IsTrue(condition);

// falseか
Assert.IsFalse(condition);

null検証

// nullか
Assert.IsNull(obj);

// nullではないか
Assert.IsNotNull(obj);

例外の検証

// 特定の例外がスローされるか
Assert.Throws<ArgumentException>(() =>
{
    // 例外をスローするコード
});

// 例外がスローされないか
Assert.DoesNotThrow(() =>
{
    // 正常に動作するコード
});

コレクションの検証

// コレクションが空か
Assert.IsEmpty(collection);

// コレクションが空ではないか
Assert.IsNotEmpty(collection);

// 要素数の確認
Assert.AreEqual(3, list.Count);

テストの実行方法

Test Runnerでの実行

Test Runnerウィンドウで、EditModeタブを選択し、テストを実行します。

実行方法:

  • すべて実行: Run Allボタンをクリック
  • 個別実行: テスト名を右クリック → Runを選択
  • 再実行: Rerun Failedボタンで失敗したテストのみ再実行

テスト結果の確認

  • 緑のチェックマーク: テスト成功
  • 赤のバツマーク: テスト失敗
  • 黄色の警告: テストスキップ

失敗したテストをクリックすると、エラーメッセージとスタックトレースが表示されます。

注意点

MonoBehaviourのテストには制限がある

EditModeテストでは、MonoBehaviourのライフサイクルメソッド(Start, Updateなど)は自動的に呼ばれません。GameObjectを使ったテストにはPlayModeテストを使用してください。

// ❌ EditModeでは動作しない
[Test]
public void TestMonoBehaviour()
{
    var go = new GameObject();
    var component = go.AddComponent<MyComponent>();
    // Start()は呼ばれない
}

テストの独立性を保つ

各テストは互いに独立している必要があります。テストの実行順序に依存しないよう、[SetUp][TearDown]を活用しましょう。

private EnemyData _enemyData;

[SetUp]
public void SetUp()
{
    // 各テストの前に実行
    _enemyData = ScriptableObject.CreateInstance<EnemyData>();
}

[TearDown]
public void TearDown()
{
    // 各テストの後に実行
    Object.DestroyImmediate(_enemyData);
}

静的変数の使用に注意

静的変数は、テスト間で状態が共有されます。テストの独立性を保つため、静的変数を使用する場合は注意が必要です。

// 静的変数をリセット
[SetUp]
public void SetUp()
{
    GameManager.Reset();
}

まとめ

Unity Test FrameworkEditModeテストは、コードの品質を保つための強力なツールです。

計算ロジック、データクラス、ユーティリティクラスなど、ゲームロジックの中核となる部分をテストすることで、リファクタリングやバグ修正を安全に行えます。テストは高速に実行できるため、開発中に頻繁に実行して、コードの変更が既存機能を壊していないか確認できます。

継続的にテストを書き、実行することで、バグの早期発見と修正が可能になり、開発効率が大幅に向上します。

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