PlayMakerの処理をTestRunnerで自動テストする方法

# PlayMakerの処理をTestRunnerで自動テストする方法

![Variables](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/96b28846-779e-406f-b308-83f21634949b.png)

PlayMakerで作成した処理のテストを行う際、シーンを手動で実行して目視で確認する方法では、工数が肥大化してしまいます。

そこで、本記事では **Unity標準のTestRunnerを活用して、PlayMakerの自動テストを行う方法** を解説します。
これにより、手動での検証を減らし、効率的にバグを発見できるようになります。

## 本記事の想定読者

本記事は、以下のような方を対象としています：

- **Unity TestRunner** で **C# のテストコード**を書いたことがある
- **PlayMaker を使ってSTYLY向けのシーンを作ったことがある**
- **PlayMakerの基本的な使い方を理解している**



## 例題: タイム整形処理

本記事では、例題として以下の仕様の処理をテストしたいものとします。

- **入力**: `float`型のレースタイム（秒）
- **出力**: `string`型の整形済みレースタイム（ミリ秒まで0埋めで表示）
- **処理名**: `TimeFormatter`

具体的なテストケースとして以下を考えました。

| 入力 (float) | 期待する出力 (string) |
|:---|---|
| `3` | `3.000` |
| `3.123456` | `3.123` |

この先の説明をシンプルにするため、テストケースはこの2つのみとします。



## PlayMaker側の処理を作る

### 1. 作業フォルダの作成

`Assets/TimeFormatter` というフォルダを作成し、その中に `TimeFormatterTest` というシーンを作成します。

### 2. `TimeFormatter` オブジェクトの作成

シーン内に `TimeFormatter` というGameObjectを作成し、PlayMakerFSMをアタッチします。

![GameObject](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/92af7cb1-edee-43b4-9c67-9a9ae1075484.png)

### 3. 変数の作成

Variablesに以下の2つの変数を作成します。

| 変数名 | 型 |
|---|---|
| InputNumber | Float |
| OutputString | String |

![Variables](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/96b28846-779e-406f-b308-83f21634949b.png)

### 4. ステートマシンの構築

以下の図のように、秒数を整形する処理を作成し、処理完了後に `Done` ステートへ遷移するようにします。

![States](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/489e88dc-feeb-40b4-9492-55748631c7f3.png)

今回は、 `Convert Float To String` アクションの `Format` に `F3` を指定することで、小数第三位までの0埋め表示を実現します。

### 5. FSMの無効化

以下の図のように `TimeFormatter` のPlayMakerFSMを無効化します。

![DisableFsm](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/098da028-a966-44a5-a088-e6155d3b296c.png)

これは、テストシーンを読み込み時にFSMが自動実行されるのを防ぐためです。
あくまでテストシーン専用の設定なので、プレハブを利用している場合、この設定をOverrideする必要はありません。


## テストコードを作る

### 1. テスト用フォルダの作成

作業フォルダ配下に `Tests` フォルダを作成し、その中に `TimeFormatterTest.cs` を作成します。

### 2. テストコードの実装

```csharp
using System;
using System.Collections;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;

namespace Segur.TimeFormatter.Tests
{
    public class TimeFormatterTest
    {
        private const string SceneName = "TimeFormatterTest";
        private const string GameObjectName = "TimeFormatter";
        private PlayMakerFSM fsm;

        [UnitySetUp]
        private IEnumerator Setup()
        {
            // シーンを探す
            var guids = AssetDatabase.FindAssets("t:scene " + SceneName);
            if (guids.Length != 1)
            {
                throw new Exception("Scene name is not unique.");
            }

            // シーンを読み込む
            var path = AssetDatabase.GUIDToAssetPath(guids[0]);
            EditorSceneManager.LoadSceneInPlayMode(path, new LoadSceneParameters(LoadSceneMode.Single));
            yield return null;

            // FSMを探す
            var gameObject = GameObject.Find(GameObjectName);
            fsm = gameObject.GetComponent<PlayMakerFSM>();
        }

        private IEnumerator RunTest(float inputNumber, string expectedOutput)
        {
            // 入力変数を設定
            fsm.FsmVariables.FindFsmFloat("InputNumber").Value = inputNumber;

            // FSMを実行
            fsm.enabled = true;

            // StateがDoneになるまで待機する。5秒でタイムアウト
            var elapsedTime = 0f;
            while (fsm.ActiveStateName != "Done" && elapsedTime < 5f)
            {
                yield return null;
                elapsedTime += Time.deltaTime;
            }

            // Stateを検証。タイムアウトしていたら不合格になる
            Assert.That(fsm.ActiveStateName, Is.EqualTo("Done"));

            // 出力変数を検証
            Assert.That(fsm.FsmVariables.FindFsmString("OutputString").RawValue, Is.EqualTo(expectedOutput));
        }

        [UnityTest]
        public IEnumerator FormatsWholeNumberCorrectly()
        {
            yield return RunTest(3f, "3.000");
        }

        [UnityTest]
        public IEnumerator FormatsDecimalNumberCorrectly()
        {
            yield return RunTest(3.12345f, "3.123");
        }
    }
}
```

このテストでは、PlayMakerの変数 (`Variables`) を介してFSMを操作し、期待通りの出力になるかを検証しています。



## PlayMode Test を有効にする設定

デフォルトでは `asmdef` を作成しないと `NUnit` の参照エラーが発生しますが、以下の設定を変更することでエラーを解消できます。

### 設定手順

`ProjectSettings/ProjectSettings.asset` をテキストエディターで開き、以下の設定を `1` に変更してください。

```yml
playModeTestRunnerEnabled: 1
```

これにより、`asmdef` なしでも PlayMode Test を実行できます。 [^1]

[^1]: この設定をするとビルドした際にテストコードが含まれてしまう恐れがあります。ただ、今回の読者はSTYLYシーン制作者を想定しており、アプリビルドはしないため、問題はないと考えています。また、シンボル定義によってこの問題は回避できることもわかっています。



## テストを実行する

TestRunnerのPlayModeタブを開き、作成したテストを実行します。

![TestRunner](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/18de45cf-b346-4c31-8406-98cdac0ae947.png)

すべてのテストが合格すれば、PlayMakerの処理が正しく動作していることが確認できます！

おつかれさまでした！


## さいごに

本記事に掲載した手法は、 @kotauchisunsun 氏が考案したアイデアをもとにしています。
この場を借りて感謝いたします。ありがとうございました。