How Unity is doing unit test
In General if you include Entities in your project and then open Test Runner you should see the list of UnitTests provided by Unity something like this:

Now you can look inside some tests and see how Unity did this tests. So lets look at some Transform Tests:
using System;
using NUnit.Framework;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
namespace Unity.Entities.Tests
{
[TestFixture]
partial class TransformTests : ECSTestsFixture
{
const float k_Tolerance = 0.01f;
[Test]
public void TRS_ChildPosition()
{
var parent = m_Manager.CreateEntity(typeof(LocalToWorld), typeof(LocalTransform));
var child = m_Manager.CreateEntity(typeof(LocalToWorld), typeof(LocalTransform), typeof(Parent));
m_Manager.SetComponentData(parent, LocalTransform.Identity);
m_Manager.SetComponentData(child, new Parent {Value = parent});
m_Manager.SetComponentData(child,
LocalTransform.FromRotation(quaternion.RotateY(math.PI))
.TransformTransform(
LocalTransform.FromPosition(new float3(0.0f, 0.0f, 1.0f))));
World.GetOrCreateSystem<ParentSystem>().Update(World.Unmanaged); // Connect parent and child
World.GetOrCreateSystem<LocalToWorldSystem>().Update(World.Unmanaged); // Write to the child's LocalToWorld
m_Manager.CompleteAllTrackedJobs();
var childWorldPosition = m_Manager.GetComponentData<LocalToWorld>(child).Position;
Assert.AreEqual(.0f, childWorldPosition.x, k_Tolerance);
Assert.AreEqual(.0f, childWorldPosition.y, k_Tolerance);
Assert.AreEqual(-1f, childWorldPosition.z, k_Tolerance);
}
}
If we look at the class definition, we see that TransformTests class is derived from ECSTestsFixture class that is special class designed by unity to simulate running system and executing EntityManager functions in a controlled test environment.
Create Custom ECSTestsFixture
If you wanted to let you project that it depends o this ECSTestsFixture class, you must modify manifest.json to let Unity know that you would like to use entities.tests assemblies.
manifest.json
{
"testables" : [
"com.unity.entities"
],
}
I have spend some time to use out of the box ECSTestFixture from Unity, but the problem is to make it work. You have to modify manifest.json and also Unity will execute its own test before yours so this will slow down whole usage. I have copyed Unity version and removed all the necessary code to keep it as small as possible. Here is the result: It can be used within your project and does not need special references.
EcsTestsFixture.cs
[BurstCompile(CompileSynchronously = true)]
public abstract class EcsTestsFixture
{
protected World m_PreviousWorld;
protected World World;
protected PlayerLoopSystem m_PreviousPlayerLoop;
protected EntityManager m_Manager;
protected EntityManager.EntityManagerDebug m_ManagerDebug;
protected int StressTestEntityCount = 1000;
private bool JobsDebuggerWasEnabled;
[SetUp]
public void Setup()
{
// unit tests preserve the current player loop to restore later, and start from a blank slate.
m_PreviousPlayerLoop = PlayerLoop.GetCurrentPlayerLoop();
PlayerLoop.SetPlayerLoop(PlayerLoop.GetDefaultPlayerLoop());
m_PreviousWorld = World.DefaultGameObjectInjectionWorld;
World = World.DefaultGameObjectInjectionWorld = CreateDefaultWorld ? DefaultWorldInitialization.Initialize("Default Test World") : new World("Empty Test World");
World.UpdateAllocatorEnableBlockFree = true;
m_Manager = World.EntityManager;
m_ManagerDebug = new EntityManager.EntityManagerDebug(m_Manager);
// Many ECS tests will only pass if the Jobs Debugger enabled;
// force it enabled for all tests, and restore the original value at teardown.
JobsDebuggerWasEnabled = JobsUtility.JobDebuggerEnabled;
JobsUtility.JobDebuggerEnabled = true;
#if (UNITY_EDITOR || DEVELOPMENT_BUILD) && !DISABLE_ENTITIES_JOURNALING
// In case entities journaling is initialized, clear it
EntitiesJournaling.Clear();
#endif
}
public bool CreateDefaultWorld = true;
[TearDown]
public void TearDown()
{
if (World != null && World.IsCreated)
{
// Clean up systems before calling CheckInternalConsistency because we might have filters etc
// holding on SharedComponentData making checks fail
while (World.Systems.Count > 0)
{
World.DestroySystemManaged(World.Systems[0]);
}
m_ManagerDebug.CheckInternalConsistency();
World.Dispose();
World = null;
World.DefaultGameObjectInjectionWorld = m_PreviousWorld;
m_PreviousWorld = null;
m_Manager = default;
}
JobsUtility.JobDebuggerEnabled = JobsDebuggerWasEnabled;
PlayerLoop.SetPlayerLoop(m_PreviousPlayerLoop);
}
[BurstDiscard]
static public void TestBurstCompiled(ref bool falseIfNot)
{
falseIfNot = false;
}
[BurstCompile(CompileSynchronously = true)]
static public bool IsBurstEnabled()
{
bool burstCompiled = true;
TestBurstCompiled(ref burstCompiled);
return burstCompiled;
}
}
Now create another child class where we will add some more method that we can then use to create nice tests. Especially one method is important to set DeltaTime in the simulation code.
ECSCustomTestsFixture.cs
// Just a wrapper to Unity internal ECSTestsFixture
// but this way you can add your own helper functions
public class CustomEcsTestsFixture : EcsTestsFixture
{
private const int EntityCount = 1000;
protected EntityManager Manager => m_Manager;
protected void UpdateSystem<T>() where T : unmanaged, ISystem => World.GetExistingSystem<T>().Update(World.Unmanaged);
protected void UpdateBaseSystem<T>() where T : SystemBase => World.GetExistingSystem<T>().Update(World.Unmanaged);
protected SystemHandle CreateSystem<T>() where T : unmanaged, ISystem => World.CreateSystem<T>();
protected Entity CreateEntity(params ComponentType[] types) => Manager.CreateEntity(types);
protected void CreateEntities(ComponentType[] types, int entityCount = EntityCount)
{
for (var i = 0; i < entityCount; i++)
Manager.CreateEntity(types);
}
/**
* This method is used to create the EntityCommandBufferSystem.
*/
protected void CreateEntityCommandBufferSystems()
{
World.CreateSystem<EndSimulationEntityCommandBufferSystem>();
}
/**
* This method is used to set the delta time of the world.
*/
protected void SetDeltaTime(float deltaTime)
{
World.SetTime(new TimeData(World.Time.ElapsedTime, deltaTime));
}
}
Now we can create our first very test class. Our test class will be testing AttractionSystem that is used to make Entity move towards defined target entity (attraction system is like magnet). I will write some more about it in the next posts. So here is the test class:
AttractionSourceSystemTest.cs
[TestFixture]
public class AttractionSourceSystemTest : CustomEcsTestsFixture
{
[SetUp]
public void Setup()
{
// This is to tell our abstract class to create DefaultWorld and not empty world
// It was because I wanted to create all other systems like Physiscs, EndSimulationEntityCommandBufferSystem and so on.
CreateDefaultWorld = true;
base.Setup();
// Here we create our AttractionSourceSystem (that we are going to test)
CreateSystem<AttractionSourceSystem>();
// Also we wanted to create our target entity we will try to move towards.
CreateTargetEntityAt();
}
AttractionSourceSystemTest.cs – CreateEntity methods
private Entity CreateTargetEntityAt(float3 targetPosition = new float3())
{
var targetEntity = Manager.CreateEntity(typeof(LocalTransform), typeof(IdentifyAttractionSourcesRequest));
MoveTargetEntityToPosition(targetEntity, targetPosition);
return targetEntity;
}
private void MoveTargetEntityToPosition(Entity targetEntity, float3 position)
{
var targetLT = Manager.GetComponentData<LocalTransform>(targetEntity);
targetLT.Position = position;
Manager.SetComponentData(targetEntity, targetLT);
}
private AttractionSourceAspect CreatePhysicsAttractionSourceEntityAndGetAspect()
{
var attractionSourceArchetype = Manager.CreateArchetype(
typeof(AttractionSource),
typeof(LocalTransform),
typeof(PhysicsVelocity),
typeof(PhysicsMass),
typeof(MovableTarget)
);
var testAttractionSource = Manager.CreateEntity(attractionSourceArchetype);
return Manager.GetAspect<AttractionSourceAspect>(testAttractionSource);
}
Now we are going to create our first Test Method.
WithPhysicsVelocity_ShouldSetLinearVelocityTowardsTarget()
[Test(Description = "AttractionSourceSystem should set LinearVelocity towards TargetEntity if target is not out of range")]
public void WithPhysicsVelocity_ShouldSetLinearVelocityTowardsTarget()
{
// we will create target entity at local pocation 10,0,0
var target = CreateTargetEntityAt(new float3(10,0,0));
// now create attractionSource entity that has AttractionSource component
var source = CreatePhysicsAttractionSourceEntityAndGetAspect();
var sourceCmp = AttractionSource.MagneticAttractionSource(1, 100, target).SetForce(1);
// set the target entity we would like to follow
Manager.SetComponentData(source.Entity, sourceCmp);
Assert.True(sourceCmp.HasTarget, "Should has true before test");
//simulate world update (delta time set to 1)
SetDeltaTime(1f);
UpdateSystem<AttractionSourceSystem>(); // this call will actuale execute jobs but only on this specific syste,
//assert what we expect to be
var sourceAspect = Manager.GetAspect<AttractionSourceAspect>(source.Entity);
Assert.True(sourceAspect.HasTarget, "Should be false");
Assert.AreEqual(10f, sourceAspect.PhysicsVelocityRW.ValueRO.Linear.x, "Object should move close to target (0,0,0) with velocity x=10 (deltaTime=1)");
// Physics did not work in unit tests
//Assert.AreEqual(sourceAspect.Position.x, 9, "Object should move close to target (0,0,0)");
}
Usually test method have 3 parts:
- Prepare – In this part you should prepare all the test situation yoy are going to test. In ECS that means you have to create all Entities + attach to them needed Components with start data values.
- Simulate – Here you should call
UpdateSystem<name_of_system>();to make ECS system execute all .Update() method on this system and execute all the logic inside it (e.g. run Jobs e.t.c.). If you wanted to make under controll also time you should call before itSetDeltaTime(1f);to set deltaTime=1f; so your calculations are precise and that you can expect some calculated result. - Verify – In this part you should get data / entities and components and verify if their values was change as you expect should be. In my case I am validating if Entity PhysicsVelocity.Linear.x was set to value 10f.
Summary

All this post looks complex but it is really not that complex. In my case I have spend some hours to make it works becuase I have problem to make running EndSimulationEntityCommandBufferSystem during Unity Test, but finally I have ralized that to make this work I need to use CreateDefaultWorld = true; because when I use empty world there must be done more setup to make EntityCommandBufferSystem to work. Also during Update you need to call UpdateBaseSystem<EndSimulationEntityCommandBufferSystem>();
Hope this examples will help you in your work with ECS, so happy coding.