System Tests: Testing with EntityCommandBufferSystems

Hi all,

I’d be grateful for some feedback on how to Test when the systems use EntityCommandBufferSystems.

This is the way I set it up:
DeathSystemTest.cs

[TestFixture]
public class DeathSystemTest : BaseTest<DeathSystem>
{
    [Test]
    public void setsDeadIfMaxStaminaIsEqualZeroTest()
    {
        var entity = em.CreateEntity(typeof(SoldierStatus));

        em.SetComponentData(
                            entity,
                            new SoldierStatus
                            {
                                maxStamina = 0
                            }
                           );

        World.Update();

        Assert.True(em.HasComponent<Dead>(entity));
    }
}

The BaseTest class:
BaseTest.cs

public class BaseTest<T> : ECSTestsFixture where T : class
{
    protected EntityManager em;
    public    T             s;

    [SetUp]
    public override void Setup()
    {
        base.Setup();

        DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(World, new List<Type>());
      
        em = m_Manager;
        s  = World.GetOrCreateSystem(typeof(T)) as T;
      
        var group = World.GetOrCreateSystem<SimulationSystemGroup>();
        group.AddSystemToUpdateList(s as ComponentSystemBase);

        World.SetTime(new TimeData(1.234, 0.016f));
    }
}

The above sets up the three basic groups, with the DeathSystem in the SimulationSystemGroup.

The system itself:
DeathSystem.cs

[UpdateInWorld(UpdateInWorld.TargetWorld.Server)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public class DeathSystem : SystemBase
{
    private EntityCommandBufferSystem endFrameBarrier;

    protected override void OnCreate()
    {
        endFrameBarrier = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    [BurstCompile]
    protected override void OnUpdate()
    {
        var commandBuffer = endFrameBarrier.CreateCommandBuffer();

        Entities.
            WithName("DeathSystem").
            WithNone<Dead>().
            ForEach(
                    (Entity entity, ref SoldierStatus status) =>
                    {
                        if (status.maxStamina <= 0)
                        {
                            commandBuffer.AddComponent<Dead>(entity);
                        }
                    }
                   ).
            Schedule();

        endFrameBarrier.AddJobHandleForProducer(Dependency);
    }
}

In short: When status.maxStamina is <= 0, the code commandBuffer.AddComponent<Dead>(entity) is run.

However, the assertion Assert.True(em.HasComponent<Dead>(entity)) fails after the World.Update(). I checked that the entity is the same as in the test.

How do I get the commandBuffer to replay before I test the assertion? I assumed that should happen automatically in the World.Update() – am I wrong?

You’ve never added EndSimulationEntityCommandBufferSystem to a system group so it’s not going to update.

Personally I still use (a copy of) ‘ECSTestsFixture’ as my base for my Unit tests and I tick systems manually instead of relying on the update loop.

1 Like

Thanks again, @tertle !

Ah, I guess the [UpdateInGroup(typeof (SimulationSystemGroup), OrderLast = true)] in EndSimulationEntityCommandBufferSystem is not automatically adhered to?

I assumed since the line World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>() is generally used, that it would also correctly add it to the right group. (But it doesn’t since now it works)

I also use ECSTestsFixture, which is the base class of my BaseTest. How do you tick them manually, including running a playback on the commandBuffer?

If you follow the default world initialization you’ll see that everything is done in
DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups
where all systems, including buffers, are added to system groups.

Simply calling GetOrCreate just gets or creates the system. It doesn’t add it to any system group.

1 Like

As for how I ticks systems manually, this is what a test would somewhat look like from me

/// <summary> Tests for <see cref="DeathSystem"/>. </summary>
public class DeathSystemTests : ECSTestsFixture
{
    private DeathSystem deathSystem;
    private EndSimulationEntityCommandBufferSystem buffer;

    [SetUp]
    public override void Setup()
    {
        base.Setup();

        this.deathSystem = this.World.GetOrCreateSystem<DeathSystem>();
        this.buffer = this.World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    [Test]
    public void setsDeadIfMaxStaminaIsEqualZeroTest()
    {
        var entity = this.m_Manager.CreateEntity(typeof(SoldierStatus));

        this.m_Manager.SetComponentData(
            entity,
            new SoldierStatus
            {
                maxStamina = 0
            }
        );

        this.deathSystem.Update();
        Assert.False(this.m_Manager.HasComponent<Dead>(entity)); // ecb hasn't run

        this.buffer.Update();
        Assert.True(this.m_Manager.HasComponent<Dead>(entity));
    }
}

Even if the update worked how you implemented, I prefer this way as it gives me control over what is updating in the test ensuring I’m only testing what I’m intending.

1 Like

Thanks a lot – my base class now looks like this:

public class BaseTest<T> : ECSTestsFixture where T : class
{
    protected EntityManager em;
    public    T             s;

    [SetUp]
    public override void Setup()
    {
        base.Setup();

        em = m_Manager;

        var systems = new List<Type>
        {
            typeof(T),
            typeof(EndSimulationEntityCommandBufferSystem)
        };
        DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(World, systems);

        World.SetTime(new TimeData(1.234, 0.016f));
    }
}

(for anybody reading this, AddSystemsToRootLevelSystemGroups also creates all systems passed to it, I think in violation of the method naming scene – since elsewhere, group.AddSystem does not, iirc)

I had quite some trouble locating where the EndSimulationEntityCommandBufferSystem is created or even referenced apart from example code, docs, or tests in the entities package. To be honest, I still don’t know how it exists before anybody calls GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>() – and even then, I’m wondering how it is added to the right group.
(So for testing, I do that in the above base class – I’d prefer some Entities package setup call where the default EntityCommandBufferSystem systems are created, will continue looking…)

It does work, but I also see your point, thank you for the example.
I mainly intended to use Update to get as close to the actual running code as possible, and to be able to test system order from the attributes UpdateInGroup etc. But I have a feeling I’ll end up having a fixed order of systems to have a clearer overview of the running systems, so your example will come in handy.