Now, let's discuss the topic of Test Driven Development with XNA.
Note: this post includes complete code sample on CodePlex.
Download code sample here.
Test Driven Development (TDD)
Test driven development (TDD) is similar to unit testing except the unit tests are written before the objects they test. TDD is gaining as a development best practice because objects are designed with testability in mind: an object and its dependencies must be loosely coupled from the outset.
TDD practitioners follow these three laws:
First Law: Second Law: Third Law: | You may not write production code unless you’ve first written a failing unit test You may not write more of a unit test than is sufficient to fail You may not write more production code than is sufficient to make the failing unit test pass |
Instead of designing a module, coding then testing, you turn the process around and do the testing first.
To put it another way, you don't write a single line of production code until you have a test that fails.
The typical programming sequence is something like this:
1. Write a test.
2. Run the test. It fails to compile because the code you're trying to test doesn't even exist yet!
This is the same thing as failing.
3. Write a bare-bones stub to make the test compile.
4. Run the test. It should fail. If it doesn't, then the test wasn't very good.
5. Implement the code to make the test pass.
6. Run the test. It should pass. If it doesn't, back up one step and try again.
7. Start over with a new test!
Example
As an example, let's revise the Going Beyond tutorial to demonstrate XNA and Test Driven Development.
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.
However, this time the unit tests will be written before the objects they test.
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 |
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: Warp Center
[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); } |
3. Write a bare-bones stub to make the test compile.
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; } // Same code as previous post. }INPUT MANAGER
public class InputManager : IInputManager { // InputManager has dependency on InputFactory. private readonly AInputFactory inputFactory; public InputManager(AInputFactory inputFactory) { this.inputFactory = inputFactory; } public Boolean Warp() { return inputFactory.Warp(); } }INPUT FACTORY
public abstract class AInputFactory { public abstract Boolean Warp(); } public class PhoneInputFactory : AInputFactory { public override Boolean Warp() { // Logic goes here. } } public class WorkInputFactory : AInputFactory { public override Boolean Warp() { // Logic goes here. } } public class XboxInputFactory : AInputFactory { public override Boolean Warp() { // Logic goes here. } }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; } // Same code as previous post. public SpaceShip SpaceShip { get { return spaceShip; } } }4. Run the test. It should fail.
5. Implement the code to make the test pass.
GAME OBJECT MANAGER
public class GameObjectManager : IGameObjectManager { // Same code as previous post. // Update each game object. public void Update(GameTime gameTime) { Boolean warp = inputManager.Warp(); spaceShip.Update(warp); } }SPACE SHIP
public class SpaceShip { // Same code as previous post. // Update warp if action is invoked. public void Update(Boolean warp) { // Warp. if (warp) { ModelPosition = Vector3.Zero; ModelVelocity = Vector3.Zero; ModelRotation = 0.0f; } } }6. Run the test. It should pass.
7. Start over with a new test!
Test #2: Rotate Left
[Test] public void Left() { // Arrange. inputManager.Stub(im => im.Rotate()).Return(-1); // Act. gameObjectManager.Update(gameTime); // Assert. Assert.AreEqual(0.1f, gameObjectManager.SpaceShip.ModelRotation); } |
3. Write a bare-bones stub to make the test compile.
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 Boolean Warp() { return inputFactory.Warp(); } }INPUT FACTORY
public abstract class AInputFactory { public abstract Single Rotate(); public abstract Boolean Warp(); } public class PhoneInputFactory : AInputFactory { public override Single Rotate() { // Logic goes here. } public override Boolean Warp() { // Logic goes here. } } public class WorkInputFactory : AInputFactory { public override Single Rotate() { // Logic goes here. } public override Boolean Warp() { // Logic goes here. } } public class XboxInputFactory : AInputFactory { public override Single Rotate() { // Logic goes here. } public override Boolean Warp() { // Logic goes here. } }4. Run the test. It should fail.
5. Implement the code to make the test pass.
GAME OBJECT MANAGER
public class GameObjectManager : IGameObjectManager { // Same code as previous post. // Update each game object. public void Update(GameTime gameTime) { Single rotate = inputManager.Rotate(); Boolean warp = inputManager.Warp(); spaceShip.Update(rotate, warp); } }SPACE SHIP
public class SpaceShip { // Same code as previous post. // Update rotate and warp if actions are invoked. public void Update(Single rotate, Boolean warp) { // Rotate. if (rotate != 0) { const Single scale = 0.10f; ModelRotation -= rotate * scale; } // Warp. if (warp) { ModelPosition = Vector3.Zero; ModelVelocity = Vector3.Zero; ModelRotation = 0.0f; } } }6. Run the test. It should pass.
7. Start over with a new test!
Test #3: Rotate Right
[Test] public void Right() { // Arrange. inputManager.Stub(im => im.Rotate()).Return(1); // Act. gameObjectManager.Update(gameTime); // Assert. Assert.AreEqual(-0.1f, gameObjectManager.SpaceShip.ModelRotation); } |
3. Write a bare-bones stub to make the test compile.
4. Run the test. It should fail.
5. Implement the code to make the test pass.
6. Run the test. It should pass.
7. Start over with a new test!
Test #4: Move Forward
[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); } |
3. Write a bare-bones stub to make the test compile.
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. } }4. Run the test. It should fail.
5. Implement the code to make the test pass.
GAME OBJECT MANAGER
public class GameObjectManager : IGameObjectManager { // Same code as previous post. // 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); } }SPACE SHIP
public class SpaceShip { // 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; } }6. Run the test. It should pass.
7. All tests complete!
Download code sample here.
Summary
The revised Going Beyond tutorial demonstrates how to integrate Unit Tests into an existing code base.
However, writing the unit tests before the code helps drive development of the systems under test.
In conclusion, agile software development does seem to have the potential to scale using XNA:
|
Extract the GameManager into the base to manage interaction between all subsystems in the game:
public abstract class BaseComponent { public GameManager GameManager { get { return gameManager; } set { if (null == gameManager) { gameManager = value; }} } private GameManager gameManager; } public class ComponentA : BaseComponent { public ComponentA() { PropertyA = "Hello"; } public void Print() { PropertyA = GameManager.ComponentB.PropertyB; } public String PropertyA { get; private set; } } public class ComponentB : BaseComponent { public ComponentB() { PropertyB = "World"; } public void Print() { PropertyB = GameManager.ComponentA.PropertyA; } public String PropertyB { get; private set; } } public class GameManager { public GameManager(ComponentA componentA, ComponentB componentB) { ComponentA = componentA; ComponentB = componentB; } public ComponentA ComponentA { get; private set; } public ComponentB ComponentB { get; private set; } }Once issues such as circular references have been resolved, then there is an opportunity to integrate more complex game code using XNA and agile software development techniques.