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:
PerformTest()
Arrange()
Act()
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:
viewAngle
valueviewRadius
value- Clearing
allVisiblePedestrians
to remove any pedestrians before the test begins - 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.