Thursday, March 17, 2011

XNA and Unit Testing

In the previous post, we discussed the topic of XNA and an IoC Container.
Now, let's build on this information to discuss Unit Testing with XNA.

Note: this post includes complete code sample on CodePlex.
Download code sample here.

Unit Testing
Unit testing is the practice in which individual units of source code are tested in isolation. Consequently, unit tests do not measure how objects interact with dependent objects; these are integration tests.

In order to successfully unit test an individual game component, external dependencies are broken, and replaced by mock objects: fake objects that emulate real classes and help test expectations about how that class should function.

Therefore, clean unit tests should be written F.I.R.S.T:
Fast
Independent
Repeatable
Self-validating
Timely
Tests should be fast
Tests should not depend on each other
Tests should be repeatable in any environment
Tests should have a Boolean output: either they pass or fail
Tests should be written in a timely fashion

Example
As an example, let's revise the Going Beyond tutorial to demonstrate XNA and Unit Testing.

Sample
The following code sample refactors the tutorial to move a 3D model using input from the controller.
In order to test the model's rotate and move methods in isolation, the external dependency on the controller must be broken.

Download the code sample from XNA and IoC Container post; Unit Tests will be added to this code.
Write all code changes first then add unit tests afterwards to assert the correct behavior.

First, identify the System Under Test: the component that will drive the unit tests: Game Object Manager
The Game Object Manager is responsible for managing all objects in the game: currently 1x spaceship.

Therefore, all unit tests will involve the interaction between the SpaceShip and input from the controller:
Action
Rotate Left
Rotate Right
Move Forward
Warp Center
Windows PC
Press left key
Press right key
Press space key
Press enter key
Windows Phone 7
Tap screen bottom left
Tap screen bottom right
Tap screen top right
Tap screen top left
Xbox 360
Move controller left
Move controller right
Press right trigger
Press A button

All logic to load, update and draw the model can be encapsulated into a single game object:
SPACE SHIP
public class SpaceShip
{
 private Model spaceShipModel;
 private Matrix[] transforms;

 public SpaceShip() : this(Vector3.Zero)
 {
 }

 public SpaceShip(Vector3 modelPosition)
 {
  ModelRotation = 0.0f;
  ModelPosition = modelPosition;
  ModelVelocity = Vector3.Zero;
 }

 // Load model and set view/projection matrices.
 public void LoadContent(Model theSpaceShipModel, Matrix viewMatrix, Matrix projectionMatrix)
 {
  // Same code as previous post.
 }

 // Update rotate, move and warp if actions are invoked.
 public void Update(Single rotate, Single move, Boolean warp)
 {
  // Rotate.
  if (rotate != 0)
  {
   const Single scale = 0.10f;
   ModelRotation -= rotate * scale;
  }

  // Move.
  if (move != 0)
  {
   // Create some velocity if move action invoked.
   Vector3 modelVelocityAdd = Vector3.Zero;

   // Find out thrust direction using rotation.
   Single sin = -(Single)Math.Sin(ModelRotation);
   Single cos = -(Single)Math.Cos(ModelRotation);

   modelVelocityAdd.X = sin;
   modelVelocityAdd.Z = cos;

   // Scale direction by the amount of movement.
   modelVelocityAdd *= move;

   // Finally, add this vector to our velocity.
   ModelVelocity += modelVelocityAdd;
  }

  // Warp.
  if (warp)
  {
   ModelPosition = Vector3.Zero;
   ModelVelocity = Vector3.Zero;
   ModelRotation = 0.0f;
  }

  // Add velocity to position and bleed off velocity over time.
  ModelPosition += ModelVelocity;
  ModelVelocity *= 0.97f;
 }

 // Draw model.
 public void Draw()
 {
  // Same code as previous post.
 }

 public Single ModelRotation { get; private set; }
 public virtual Vector3 ModelPosition { get; private set; }
 public Vector3 ModelVelocity { get; private set; }
}
Next, update the Game Object Manager: detect input and set the rotate, move and warp values:
GAME OBJECT MANAGER
public class GameObjectManager : IGameObjectManager
{
 // GameObjectManager has dependency on CameraManager, ContentManager, InputManager and SpaceShip.
 private readonly ICameraManager cameraManager;
 private readonly IContentManager contentManager;
 private readonly IInputManager inputManager;
 private readonly SpaceShip spaceShip;

 public GameObjectManager(ICameraManager cameraManager, IContentManager contentManager, IInputManager inputManager, SpaceShip spaceShip)
 {
  this.cameraManager = cameraManager;
  this.contentManager = contentManager;
  this.inputManager = inputManager;
  this.spaceShip = spaceShip;
 }

 // Load content for each game object.
 public void LoadContent()
 {
  spaceShip.LoadContent(contentManager.SpaceShipModel, cameraManager.ViewMatrix, cameraManager.ProjectionMatrix);
 }

 // Update each game object.
 public void Update(GameTime gameTime)
 {
  Single rotate = inputManager.Rotate();
  Single move = inputManager.Move();
  Boolean warp = inputManager.Warp();

  spaceShip.Update(rotate, move, warp);
 }

 // Draw each game object.
 public void Draw()
 {
  spaceShip.Draw();
 }

 public SpaceShip SpaceShip { get { return spaceShip; } }
}
Finally, input detection: each device will have its own rotate, move and warp implementation:
INPUT MANAGER
public class InputManager : IInputManager
{
 // InputManager has dependency on InputFactory.
 private readonly AInputFactory inputFactory;

 public InputManager(AInputFactory inputFactory)
 {
  this.inputFactory = inputFactory;
 }

 public Single Rotate() { return inputFactory.Rotate(); }
 public Single Move() { return inputFactory.Move(); }
 public Boolean Warp() { return inputFactory.Warp(); }
}
INPUT FACTORY
public abstract class AInputFactory
{
 public abstract Single Rotate();
 public abstract Single Move();
 public abstract Boolean Warp();
}

public class PhoneInputFactory : AInputFactory
{
 public override Single Rotate() { // Logic goes here. }
 public override Single Move() { // Logic goes here. }
 public override Boolean Warp() { // Logic goes here. }
}

public class WorkInputFactory : AInputFactory
{
 public override Single Rotate() { // Logic goes here. }
 public override Single Move() { // Logic goes here. }
 public override Boolean Warp() { // Logic goes here. }
}

public class XboxInputFactory : AInputFactory
{
 public override Single Rotate() { // Logic goes here. }
 public override Single Move() { // Logic goes here. }
 public override Boolean Warp() { // Logic goes here. }
}

Unit Tests
Write the unit tests, one for each action: Rotate Left, Rotate Right, Move Forward and Warp Center.

First, add New Windows Game Library project to the solution; this project will contain the unit tests.
Next, add references to the following managed libraries: NUnit Framework and Rhino Mocks.

Next, add one test fixture for the system under test: Game Object Manager
Note: external dependencies are broken and replaced by mock objects:
[TestFixture]
public class GameObjectManagerTests
{
 // System under test.
 private IGameObjectManager gameObjectManager;

 private ICameraManager cameraManager;
 private IContentManager contentManager;
 private IInputManager inputManager;
 private SpaceShip spaceShip;
 private readonly GameTime gameTime = new GameTime();

 [SetUp]
 public void SetUp()
 {
  cameraManager = MockRepository.GenerateStub<ICameraManager>();
  contentManager = MockRepository.GenerateStub<IContentManager>();
  inputManager = MockRepository.GenerateStub<IInputManager>();
  spaceShip = new SpaceShip();

  gameObjectManager = new GameObjectManager(
   cameraManager,
   contentManager,
   inputManager,
   spaceShip);
 }
}
Test #1: Rotate Left
  • Simulate input from the controller to return the Rotate Left action
  • Update the Game Object Manager which updates the SpaceShip
  • Assert the rotation of the SpaceShip updates correctly
[Test]
public void Left()
{
 // Arrange.
 inputManager.Stub(im => im.Rotate()).Return(-1);

 // Act.
 gameObjectManager.Update(gameTime);

 // Assert.
 Assert.AreEqual(0.1f, gameObjectManager.SpaceShip.ModelRotation);
}
Test #2: Rotate Right
  • Simulate input from the controller to return the Rotate Right action
  • Update the Game Object Manager which updates the SpaceShip
  • Assert the rotation of the SpaceShip updates correctly
[Test]
public void Right()
{
 // Arrange.
 inputManager.Stub(im => im.Rotate()).Return(1);

 // Act.
 gameObjectManager.Update(gameTime);

 // Assert.
 Assert.AreEqual(-0.1f, gameObjectManager.SpaceShip.ModelRotation);
}
Test #3: Move Forward
  • Simulate input from the controller to return the Move Forward action
  • Update the Game Object Manager which updates the SpaceShip
  • Assert the position of the SpaceShip updates correctly
[Test]
public void Move()
{
 // Arrange.
 inputManager.Stub(im => im.Move()).Return(1);

 // Act.
 gameObjectManager.Update(gameTime);

 // Assert.
 Assert.AreEqual(-1.0f, gameObjectManager.SpaceShip.ModelPosition.Z);
}
Test #4: Warp Center
  • Simulate input from the controller to return the Warp Center action
  • Update the Game Object Manager which updates the SpaceShip
  • Assert the position of the SpaceShip updates correctly
[Test]
public void Warp()
{
 // Arrange.
 Vector3 modelPostion = new Vector3(10, 20, 30);
 spaceShip = new SpaceShip(modelPostion);

 gameObjectManager = new GameObjectManager(
  cameraManager,
  contentManager,
  inputManager,
  spaceShip);

 inputManager.Stub(im => im.Warp()).Return(true);

 // Act.
 gameObjectManager.Update(gameTime);

 // Assert.
 Assert.AreEqual(Vector3.Zero, gameObjectManager.SpaceShip.ModelPosition);
}
Download code sample here.

Summary
The revised Going Beyond tutorial demonstrates how to integrate Unit Tests into an existing code base.
However, simply adding unit tests after the code is written does not guarantee bug free code!

For example: subtle bugs may be introduced in code that are not caught when tested in isolation simply because the tests may return false positive results.

Unit tests state what you expect the code to do. Therefore, a better approach is to write the tests first: Writing the test before the code should uncover issues quicker because assertions in the test can fail.

The practice of writing unit tests before the objects they test is called: Test Driven Development.
This will be the topic in the next post.

2 comments:

Sean said...

Thanks for the post! :D

FYI: New location of Rhino Mocks is: http://hibernatingrhinos.com/open-source/rhino-mocks

DevRabbit IT Solutions Inc. said...

Hello,
The Article on XNA and Unit Testing is very informative. It give detail information about it .Thanks for Sharing the information on Unit Testing and XNA. Software Testing Services

Post a Comment