Unity Test Frameworkとは?

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

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

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

EditModeテストとは?

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

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

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

PlayModeテストとの違い

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

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

導入方法

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

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

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

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

Test Runnerウィンドウの開き方

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

Window → General → Test Runnerでウィンドウを開きます。

テスト用フォルダの作成

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

  1. ウィンドウで右クリック
  2. Create → Testing → Tests 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);
    }
}

データクラスのテスト

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

テスト対象のクラス:

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 FrameworkのEditModeテストは、コードの品質を保つための強力なツールです。

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

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

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