Unit Testing in Wave Engine

Unit Testing is a very useful technique when developing games. It offers a lot of benefits as automated tests that can be run on a Continuous Integration server, avoid bug regressions, and so on.

This article describes how we can use the Humble Object pattern to avoid some dependencies that will help us to write useful unit test for our game.

Imagine you have a behavior like this:

  
public class ShipBehavior : Behavior
{
    private TimeSpan shootRate;        

	[RequiredComponent]
	public Transform2D Transform;

	[DataMember]
	public int BulletsLeft { get; set; }

	public ShipBehavior()
		: base()
	{           
	}

	protected override void Update(TimeSpan gameTime)
	{
		var keyboardState = WaveServices.Input.KeyboardState;
		var mouseState = WaveServices.Input.MouseState;

		this.Transform.X = mouseState.X;
		this.Transform.Y = mouseState.Y;

		if (shootRate > TimeSpan.Zero)
		{
			shootRate -= gameTime;
		}

		if (mouseState.LeftButton == ButtonState.Pressed)
		{
			if (BulletsLeft > 0 && shootRate <= TimeSpan.Zero)
			{
				shootRate = TimeSpan.FromMilliseconds(250);
				BulletsLeft--;
				var bullet = new Entity("bullet" + BulletsLeft.ToString())
					 .AddComponent(new Transform2D())
					 .AddComponent(new Sprite(WaveContent.Assets.Bullet_png))
					 .AddComponent(new SpriteRenderer());


				bullet.FindComponent<Transform2D>().Position = this.Transform.Position;
				bullet.AddComponent(new BulletBehavior());

				EntityManager.Add(bullet);
			}
		}
		if (mouseState.RightButton == ButtonState.Pressed)
		{
			BulletsLeft = 5;
		}
	}
}

The behavior manages the movement of a ship with the mouse, fires and reloads with mouse buttons.

If you want to test the shoot behavior you will have to deal with some dependencies that seem impossible to manage. For this purposes you can use the Humble Object design pattern.

Humble Object

 

The idea behind this pattern is to extract the logic you want to test by decoupling it from those dependencies to a component that is easier to test.

We are going to test only the shoot logic. You can see in the code that it is only possible to shoot when there are bullets and after a firing rate.

So, the “impossible dependency” looks like to be the “bullet” entity that is constructed on every shoot, so we will extract that code to a method on the same “ShipBehavior” class that will be our “Humble Object”.

  
var bullet = new Entity()
        .AddComponent(new Transform2D())
        .AddComponent(new Sprite(WaveContent.Assets.Bullet_png))
        .AddComponent(new SpriteRenderer());

bullet.FindComponent<Transform2D>().Position = this.Transform.Position;
bullet.FindComponent<Transform2D>().Origin = Vector2.One / 2;
bullet.FindComponent<Transform2D>().DrawOrder = 10;


bullet.AddComponent(new BulletBehavior());

EntityManager.Add(bullet);

That is the dependency we will need to extract to the interface IFire that our “Humble Object” must implement to be “mockeable” on the new component called GunController. .

We will extract the logic to check if we can fire, the reload feature and the counter for the firing rate. So after some refactoring, the ShipBehavior class looks like this:

  
public class ShipBehavior : Behavior, IFire
{
    public GunController gunController;

    [RequiredComponent]
    public Transform2D Transform;

    protected override void DefaultValues()
    {
        base.DefaultValues();

        gunController = new GunController();
        gunController.fireController = this;
    }

    protected override void Update(TimeSpan gameTime)
    {
        var keyboardState = WaveServices.Input.KeyboardState;
        var mouseState = WaveServices.Input.MouseState;

        this.Transform.X = mouseState.X;
        this.Transform.Y = mouseState.Y;

        gunController.CountShooRate(gameTime);

        if (mouseState.LeftButton == ButtonState.Pressed)
        {
            gunController.ApplyFire();
        }
        if (mouseState.RightButton == ButtonState.Pressed)
        {
            gunController.Reload();
        }
    }

    public void Fire()
    {
        var bullet = new Entity()
                .AddComponent(new Transform2D())
                .AddComponent(new Sprite(WaveContent.Assets.Bullet_png))
                .AddComponent(new SpriteRenderer());

        bullet.FindComponent<Transform2D>().Position = this.Transform.Position;
        bullet.FindComponent<Transform2D>().Origin = Vector2.One / 2;
        bullet.FindComponent<Transform2D>().DrawOrder = 10;


        bullet.AddComponent(new BulletBehavior());

        EntityManager.Add(bullet);
    }
}


The class GunController is a normal class with no dependencies that we can easily test:

  
public class GunController
{
    public int bulletsLeft = 5;
    private TimeSpan shootRate;

    public IFire fireController;

    public void ApplyFire()
    {
        if (bulletsLeft > 0 && shootRate <= TimeSpan.Zero)
        {
            shootRate = TimeSpan.FromMilliseconds(250);
            bulletsLeft--;
            fireController.Fire();
        }
    }

    public void Reload()
    {
        bulletsLeft = 5;
    }

    internal void CountShooRate(TimeSpan gameTime)
    {
        if (shootRate > TimeSpan.Zero)
        {
            shootRate -= gameTime;
        }
    }
}

We can now unit test the shoot behavior very easy:

  
[TestMethod]
public void IfThereAreBulletsAShootSucceed()
{
    Mock<IFire> fireControllerMock = new Mock<IFire>();

    GunController controller = new GunController();
    controller.fireController = fireControllerMock.Object;

    controller.ApplyFire();

    Assert.AreEqual(4, controller.bulletsLeft);

    fireControllerMock.Verify(c => c.Fire());
}


If we look at the “Humble Object” diagram we can identify every component:

Humble Object Correspondence

Download the project here and happy testing.

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *