How to Automate Testing of PlayMaker Processes with TestRunner

This article is a translated version of my original post on Qiita. Original (Japanese): https://qiita.com/segur/items/6841982a101ef6ae61b0

Variables

Testing processes created with PlayMaker manually by running scenes and visually confirming them can significantly inflate labor costs.

In this article, I will explain how to use Unity's standard TestRunner to automate testing of PlayMaker processes. This will help you reduce manual verification, effectively discovering bugs more efficiently.

Intended Audience for This Article

This article targets the following audiences:

Example: Time Formatting Process

In this article, as an example, we want to test the process with the following specifications:

We have considered the following specific test cases:

Input (float) Expected Output (string)
3 3.000
3.123456 3.123

To keep the explanation simple, there are only two test cases.

Creating the PlayMaker Process

1. Create a Working Folder

Create a folder named Assets/TimeFormatter and within it, make a scene called TimeFormatterTest.

2. Create the TimeFormatter Object

Create a GameObject called TimeFormatter in the scene and attach a PlayMakerFSM to it.

GameObject

3. Create Variables

Create the following two variables in the Variables section.

Variable Name Type
InputNumber Float
OutputString String

Variables

4. Build the State Machine

Create the process to format the seconds as shown in the diagram below, and make it transition to the Done state upon completion.

States

In this case, specify F3 in the Format field of the Convert Float To String action to achieve zero-padding display up to the third decimal place.

5. Disable the FSM

Disable the PlayMakerFSM of TimeFormatter as shown in the diagram below.

DisableFsm

This prevents the FSM from auto-executing when loading the test scene. This is a test scene-specific setting, so if you are using prefabs, you do not need to override this setting.

Creating the Test Code

1. Create a Test Folder

Create a Tests folder under the working folder, and within it, create TimeFormatterTest.cs.

2. Implement the Test Code

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()
        {
            // Find the scene
            var guids = AssetDatabase.FindAssets("t:scene " + SceneName);
            if (guids.Length != 1)
            {
                throw new Exception("Scene name is not unique.");
            }

            // Load the scene
            var path = AssetDatabase.GUIDToAssetPath(guids[0]);
            EditorSceneManager.LoadSceneInPlayMode(path, new LoadSceneParameters(LoadSceneMode.Single));
            yield return null;

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

        private IEnumerator RunTest(float inputNumber, string expectedOutput)
        {
            // Set the input variable
            fsm.FsmVariables.FindFsmFloat("InputNumber").Value = inputNumber;

            // Execute the FSM
            fsm.enabled = true;

            // Wait until the state is Done. Timeout after 5 seconds
            var elapsedTime = 0f;
            while (fsm.ActiveStateName != "Done" && elapsedTime < 5f)
            {
                yield return null;
                elapsedTime += Time.deltaTime;
            }

            // Validate the state. It fails if it times out
            Assert.That(fsm.ActiveStateName, Is.EqualTo("Done"));

            // Validate the output variable
            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");
        }
    }
}

In this test, we manipulate the FSM through PlayMaker's Variables and check if it produces the expected output.

Enabling PlayMode Test Settings

By default, if you do not create an asmdef, you will encounter reference errors with NUnit. However, you can resolve the error by changing the following settings.

Configuration Steps

Open ProjectSettings/ProjectSettings.asset in a text editor and change the following setting to 1:

playModeTestRunnerEnabled: 1

This allows you to run PlayMode Tests without asmdef. [^1]

[^1]: Enabling this setting might cause test codes to be included when building; however, considering the intended audience of this article (STYLY scene creators who won't create app builds), this shouldn't be a problem. Additionally, this issue can be avoided via symbol definitions.

Running the Tests

Open the PlayMode tab of TestRunner and execute the created tests.

TestRunner

If all tests pass, you can confirm that the PlayMaker process is functioning correctly!

Congratulations on your hard work!

In Conclusion

The method described in this article is based on the idea devised by @kotauchisunsun. I would like to take this opportunity to express my gratitude. Thank you very much!