Skip to content

Evacu-agent testing strategy

Overview

Testing is covered through unit tests as Unity play tests, these use the Scene Evacu-agent to run tests though this scene does not need to be active when running tests from the test runner.

All Evacu-agent unit tests can be found in Assets/Tests/EvacuAgentTests.

Each test is modelled as its own class extending the abstract base class ArrangeActAssertStrategy which extends from EvacuAgentCommonSceneTest.

ArrangeActAssertStrategy

Contains the methods:

  1. PerformTest()
  2. Arrange()
  3. Act()
  4. Assertion()

Example test class structure

As each test is a class all variables should be declared as class variables.

Arrange() should contain all setup logic, for example spawning or placing pedestrians.

Act() should contain only the method that is under test, however some extra setup logic may be required here due to Order of execution for event functions. See exceptions below for more detail.

Assertion() should contain all assertions, which will check values returned from the method under test to expected values declared as class variables and populated in Arrange().

public class BoidSeparationComponent_ReturnsVelocityOfZero_WhenNeighboursCountIsZero : ArrangeActAssertStrategy
{
    private EvacuAgentPedestrianBase evacuAgentPedestrianBase;
    private BoidSeparationComponent boidSeparationComponent;
    private FriendGroupBoidBehaviour friendGroupBoidBehaviour;
    private Vector3 actualBoidSeparationComponentResult;
    private Vector3 expectedBoidSeparationComponentResult;

    [UnityTest]
    public override IEnumerator PerformTest()
    {
        Arrange();
        yield return null;
        Act();
        Assertion();
    }

    public override void Arrange()
    {
        evacuAgentPedestrianBase = SpawnFriendGroupOfEvacuAgentPedestrians(1).First();
        friendGroupBoidBehaviour = evacuAgentPedestrianBase.GetComponentInChildren<FriendGroupBoidBehaviour>();
        boidSeparationComponent = evacuAgentPedestrianBase.GetComponentInChildren<BoidSeparationComponent>();
        expectedBoidSeparationComponentResult = Vector3.zero;
    }

    public override void Act()
    {
        actualBoidSeparationComponentResult = boidSeparationComponent.CalculateComponentVelocity(friendGroupBoidBehaviour);
    }

    public override void Assertion()
    {
        Assert.Zero(friendGroupBoidBehaviour.Neighbours.Count);
        Assert.AreEqual(expectedBoidSeparationComponentResult, actualBoidSeparationComponentResult);
    }
}

Sharing logic between test classes as static helpers

Related test classes can require similar setup logic or helper methods which can be shared. This is mainly achieved through the use of static helper classes as seen below.

In this case all FieldOfView tests require at an viewing Pedestrian and a targets to be detected that are within the viewAngle and viewRadius.

The method SetUpViewingGameObject sets up the FieldOfView with the following shared characteristics for each test class:

  1. viewAngle value
  2. viewRadius value
  3. Clearing allVisiblePedestrians to remove any pedestrians before the test begins
  4. The layer the viewing Pedestrian should detect
public static class FieldOfViewTestsHelper
{
    public static Vector3 viewingObjectPosition = new Vector3(0, 0, 0);
    public const string pedestrianLayerMask = "Pedestrian";

    private static string fieldOfViewPrefabPath = $"{EvacuAgentSceneParamaters.RESEOURCES_PREFABS_PREFIX}FieldOfView";

    public static FieldOfView SetUpViewingGameObject()
    {
        float defaultViewAngle = 90f;
        float defaultViewRadius = 100f;

        GameObject viewingFovGameObject = (GameObject)GameObject.Instantiate(Resources.Load(fieldFOViewPrefabPath));
        FieldOfView viewingFov = viewingFovGameObject.GetComponent<FieldOfView>();
        viewingFov.StopAllCoroutines();

        viewingFov.viewAngle = defaultViewAngle;
        viewingFov.viewRadius = defaultViewRadius;
        viewingFov.allVisiblePedestrians.Clear();
        viewingFovGameObject.layer = LayerMask.NameToLayer(pedestrianLayerMask);

        return viewingFov;
    }

    public static GameObject SetUpNonViewingObject(Vector3 position, string layerMask = pedestrianLayerMask)
    {
        GameObject nonPedestrianObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
        nonPedestrianObject.transform.localScale = new Vector3(2, 2, 3);
        nonPedestrianObject.transform.position = position;
        nonPedestrianObject.AddComponent<Pedestrian>().enabled = false;
        nonPedestrianObject.layer = LayerMask.NameToLayer(layerMask);

        return nonPedestrianObject;
    }
}

Sharing logic between test classes through EvacuAgentCommonSceneTest

Where logic is necessary for the majority of test cases, or for large groups of unrelated code where static classes would become too messy EvacuAgentCommonSceneTest can be used.

The example below is an example method extracted from EvacuAgentCommonSceneTest for instantiating a collection of Pedestrian.

public static List<EvacuAgentPedestrianBase> SpawnFriendGroupOfEvacuAgentPedestrians(int numberInGroup)
{
    List<EvacuAgentPedestrianBase> friendGroup = new List<EvacuAgentPedestrianBase>();

    // Minus 1 as the groups are spawned as one leader + a number of group members so we need to take away one for the leader
    int followerNumber = numberInGroup - 1;

    // Adjust the factory follower counts. Values need to be saved for resetting after logic is performed
    int initialMinumum = EvacuAgentSceneParamaters.FRIEND_GROUP_FOLLOWER_COUNT_MINIMUM;
    int initialMaximum = EvacuAgentSceneParamaters.FRIEND_GROUP_FOLLOWER_COUNT_MAXIMUM;

    EvacuAgentSceneParamaters.FRIEND_GROUP_FOLLOWER_COUNT_MINIMUM = followerNumber;
    EvacuAgentSceneParamaters.FRIEND_GROUP_FOLLOWER_COUNT_MAXIMUM = followerNumber;

    // Get the factory if the reference is null
    if(friendGroupLeaderFollowerPedestrianFactory == null)
       friendGroupLeaderFollowerPedestrianFactory = (FriendGroupLeaderFollowerPedestrianFactory)GameObject.FindObjectOfType(typeof(FriendGroupLeaderFollowerPedestrianFactory));

    for(int index = 0; index < numberInGroup; index++)
    {
        // Create a GameObject with a Pedestrian script to be passed into the factory
        Pedestrian pedestrian = SpawnGameObjectWithInactivePedestrianScript().GetComponent<Pedestrian>();
        EvacuAgentPedestrianBase evacuAgentPedestrianBase = friendGroupLeaderFollowerPedestrianFactory.CreateEvacuAgentPedestrian(pedestrian);

        // Turn off EvacuAgent pedestrian behaviours and field of view
        evacuAgentPedestrianBase.fieldOfView.StopAllCoroutines();
        evacuAgentPedestrianBase.behaviourController.isUpdateOn = false;
        evacuAgentPedestrianBase.GetComponentInChildren<BehaviourCollection>().enabled = false;

        friendGroup.Add(evacuAgentPedestrianBase);
    }

    // Reset follower values
    EvacuAgentSceneParamaters.FRIEND_GROUP_FOLLOWER_COUNT_MAXIMUM = initialMaximum;
    EvacuAgentSceneParamaters.FRIEND_GROUP_FOLLOWER_COUNT_MINIMUM = initialMinumum;

    return friendGroup;
}

Exceptions

Yield placement in each test

The placement of the yield statement can change in each test, it usually occurs before or after Arrange() due to the Order of execution for event functions.

A yield statement placed before Arrange() means that Start() will not be called on any scripts in Arrange(), however Awake() will be.

Yield before Arrange()

An example of using a yield statement before Arrange() can be found in SimulationPanelControllerTests in the test case TogglePedestrianFieldOfViewVisuals_CorrectlyDisablesFieldOfViewMeshRenderers. In this test FieldOfView of several pedestrians is toggled to check that a button on the user interface works. In order to do this we do not need to allow FieldOfView.Start() to run and so the yield is placed before Arrange().

Yield after Arrange()

An example of where using a yield after Arrange() is necessary is FollowerDestinationUpdateBehaviour_PerformBehaviour_CorrectlySetsNewDestination. This test checks that a pedestrians' NavMeshAgent.Destination is updated after they reach their current destination. Here the yield must be after Arrange() as the setting the destination requires releasing control back to Unity from the current method. If yield was placed before Arrange() then NavMeshAgent.Destination would be equal to Vector3(0f, 0f, 0f).

Timer based yields

Timer-based yields can also be used in special cases where coroutines that use real world time (in seconds). An example is GenericEnterLeaveBuildingBehaviour_ShouldTriggerBehaviour_EnterBuildingCoolDownResetsAfterTime. This test checks that a pedestrians' cooldown to enter a building resets after a given time in seconds. Here yield return new WaitForSeconds(secondsToWaitAfterTriggeringCoolDown) is used after Act() to ensure that Assertion() is called a given number of seconds after Act() is completed.

Assertions in Arrange()

To ensure that given properties are altered by the method under test, some test cases include assertions in Arrange(). In these cases the assertions occur at the end of Arrange() after setup logic is complete. An example of this use can be found in SimulationPanelController_TogglePedestrianFieldOfViewVisuals_CorrectlyDisablesFieldOfViewMeshRenderers where a collection of FieldOfView is first checked to be disabled. After the method under test is called regular assertions follow in Assertion().

Assertions with Vector3

A useful method in EvacuAgentCommonSceneTest is AssertTwoVectorsAreEqualWithinTolerance() which can be used to assert two Vector3 are the same.

If any component of the expected vector does not match the actual vector then the test will fail and the component that is failing will be printed to the unity console within the error section. An example of this message is:

AssertTwoVectorsAreEqualWithinTolerance - Failure in X component. Message: {e}

Where {e} will be replaced with the exception message thrown by the test.

Adding a new test

See Testing Traffic3D where the correct folder is:

  Traffic3D/Assets/Tests/EvacuAgentTests

Changing the test scene

The scene that tests occurs in can be any scene, but note that if a test retrieves a component through Unity's GameObject.Find it either needs to exist in the scene hierarchy or be instantiated during the test first.

The test scene is loaded in EvacuAgentCommonSceneTest in SetUpTest() where SceneManager.LoadScene(2); takes an integer for buildIndex. This index can be changed to edit the scene that is loaded for Evacu-agent tests.

The current test scene loading logic in EvacuAgentCommonSceneTest:

[SetUp]
public override void SetUpTest()
{
    try
    {
        SocketManager.GetInstance().SetSocket(new MockSocket());
        SceneManager.LoadScene(2);
    }
    catch (Exception e)
    {
        Debug.Log(e);
    }
}

Ensuring pedestrian behaviours are not executed

BehaviourController contains a property isUpdateOn which can be set to false via EvacuAgentPedestrianBase.behaviourController = fasle for test cases where this is appropriate.