Learning Unit Testing VIII – Emergent Design

I’ve been bracing myself for this moment since I started the project – I’m coming to believe that I’ve been heading in a slightly wrong direction.

Part of TDD is the idea of ‘emergent design’, and letting the correct design emerge from the process of writing minimal code to pass tests. At the same time, it’s necessary to keep an eye on the bigger picture so you know where exactly the code should be written.

I’ve been making the MoveModel method into a huge monster, that checks all sorts of things, and as I started investigating how to implement line of sight, I realised that this functionality should probably be moved out into some sort of utility class. MoveModel itself is now checking distances between two points (this will be duplicated later), proximity detection (probably to be used later), and as I said – was about to take on line of sight. All these details could be worked out mathematically as I was doing them, but some frameworks (such as XNA) might provide better ways to calculate them, with things such as Rays, or… something else XNAish.

The best way to handle that would be to refactor all the ‘utility’ calculations into a separate class, exposed via an interface so that it can be swapped out. That way, I can abstract out and mock all the calculations in MoveModel to deal more with the situation parameters and appropriate responses and less about the calculations themselves. At the same time, I can write tests for this new utility class so that I can test the purely mathematical aspects of the same calculations. The interactions become easier (true, false, and integer responses rather than the broad spectrum of mathematical output) since I don’t have to test multiple configurations and situation-specific responses, just the two separate things that should be behaving.

The other benefit will be that if I eventually do take this library over to an XNA frontend, I can swap out that mathematical calculator for something more efficient for the framework to use. But the basic mathematical way will still be available if I choose instead to port it to Silverlight.

I also took this opportunity to move the X, Y and Z co-ordinates into a separate class – LocationPoint.

Here’s the set of tests for MoveModel:

[TestClass]
public class ModelTests
{
[TestMethod]
public void NewTurn_SetsDistanceMovedToZero()
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model testModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
testModel.TotalDistanceMoved = 4;
testModel.NewTurn();
Assert.AreEqual(0, testModel.TotalDistanceMoved);
}
[TestMethod]
public void MoveModel_PositionHasChanged()
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>());
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model testModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
float newX = 2;
float newY = 2;
float newZ = 2;
testModel.MoveModel(newX, newY, newZ);
Assert.AreEqual(2, testModel.Location.X);
Assert.AreEqual(2, testModel.Location.Y);
Assert.AreEqual(2, testModel.Location.Z);
Assert.IsFalse(testModel.IsRunning);
}
[TestMethod]
public void MoveModel_TotalMovementInASingleTurnCannotExceedModelMovement_ThrowsMovementException()
{
bool correctExceptionThrown = false;
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>());
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model testModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
float newX = 5;
float newY = 4;
float newZ = 3;
testModel.MoveModel(newX, newY, newZ);
try
{
testModel.MoveModel(0, 0, 0);
}
catch (MovementException ex)
{
correctExceptionThrown = true;
}
Assert.IsTrue(correctExceptionThrown);
}
[TestMethod]
public void MoveModel_MakeTwoSmallMovementsWithoutGoingOverMovementRate()
{
bool correctExceptionThrown = false;
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>());
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model testModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
float newX = 1;
float newY = 1;
float newZ = 0;
try
{
testModel.MoveModel(newX, newY, newZ);
testModel.MoveModel(0, 0, 0);
}
catch (MovementException ex)
{
correctExceptionThrown = true;
}
Assert.IsFalse(correctExceptionThrown);
}
[TestMethod]
public void MoveModel_CannotMoveFurtherThanModelsMovement()
{
bool correctExceptionThrown = false;
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model testModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
float newX = 5;
float newY = 6;
float newZ = 3;
try
{
testModel.MoveModel(newX, newY, newZ);
}
catch (MovementException ex)
{
correctExceptionThrown = true;
}
Assert.IsTrue(correctExceptionThrown);
}
[TestMethod]
public void MoveModel_MovesDistanceOverMovementRateAndSetsIsRunningFlag()
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>());
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model testModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
float newX = 5;
float newY = 4;
float newZ = 3;
testModel.MoveModel(newX, newY, newZ);
Assert.IsTrue(testModel.IsRunning);
}
[TestMethod]
public void MoveModel_StopsRunningWithin8InchesOfEnemyModel()
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model enemyModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
enemyModel.Location.X = 9;
enemyModel.Location.Y = 0;
enemyModel.Location.Z = 0;
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>() { enemyModel });
Model testModel = new Model(2, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
bool correctExceptionThrown = false;
try
{
mockObjectManager.Setup(item => item.GetDistanceBetween(testModel.Location, It.IsAny<float>(), It.IsAny<float>(), It.IsAny<float>())).Returns(8);
mockObjectManager.Setup(item => item.GetPointOfIntersection(testModel.Location, It.IsAny<LocationPoint>(), enemyModel.Location, 8)).Returns(new LocationPoint(1, 0, 0));
mockObjectManager.Setup(item => item.GetLineOfSight(testModel, enemyModel)).Returns(1);
testModel.MoveModel(8, 0, 0);
}
catch (MovementException ex)
{
correctExceptionThrown = true;
Assert.AreEqual(1, ex.FinalPosition.X);
Assert.AreEqual(0, ex.FinalPosition.Y);
Assert.AreEqual(0, ex.FinalPosition.Z);
}
Assert.IsTrue(correctExceptionThrown);
Assert.AreEqual(1, testModel.Location.X);
Assert.AreEqual(0, testModel.Location.Y);
Assert.AreEqual(0, testModel.Location.Z);
}
[TestMethod]
public void MoveModel_ContinuesRunningWithEnemyMoreThan8InchesAway()
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model enemyModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
enemyModel.Location.X = 18;
enemyModel.Location.Y = 0;
enemyModel.Location.Z = 0;
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>() { enemyModel });
Model testModel = new Model(2, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
bool correctExceptionThrown = false;
try
{
testModel.MoveModel(8, 0, 0);
}
catch (MovementException ex)
{
correctExceptionThrown = true;
}
Assert.IsFalse(correctExceptionThrown);
Assert.AreEqual(8, testModel.Location.X);
Assert.AreEqual(0, testModel.Location.Y);
Assert.AreEqual(0, testModel.Location.Z);
}
[TestMethod]
public void MoveModel_ContinuesMovingWithin8InchesOfEnemyModelNotRunning()
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model enemyModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
enemyModel.Location.X = 9;
enemyModel.Location.Y = 0;
enemyModel.Location.Z = 0;
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>() { enemyModel });
Model testModel = new Model(2, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
bool correctExceptionThrown = false;
try
{
testModel.MoveModel(3, 0, 0);
}
catch (MovementException ex)
{
correctExceptionThrown = true;
}
Assert.IsFalse(correctExceptionThrown);
Assert.AreEqual(3, testModel.Location.X);
Assert.AreEqual(0, testModel.Location.Y);
Assert.AreEqual(0, testModel.Location.Z);
}
[TestMethod]
public void MoveModel_ContinuesRunningWithin8InchesOfFriendlyModel()
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model friendlyModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
friendlyModel.Location.X = 9;
friendlyModel.Location.Y = 0;
friendlyModel.Location.Z = 0;
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>() { friendlyModel });
Model testModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
bool correctExceptionThrown = false;
try
{
testModel.MoveModel(8, 0, 0);
}
catch (MovementException ex)
{
correctExceptionThrown = true;
}
Assert.IsFalse(correctExceptionThrown);
Assert.AreEqual(8, testModel.Location.X);
Assert.AreEqual(0, testModel.Location.Y);
Assert.AreEqual(0, testModel.Location.Z);
}
[TestMethod]
public void MoveModel_ContinuesRunningWithin8InchesOfEnemyModelIfOutOfSight()
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
Model enemyModel = new Model(1, mockGameManager.Object, mockObjectManager.Object);
enemyModel.Location.X = 12;
enemyModel.Location.Y = 0;
enemyModel.Location.Z = 0;
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>() { enemyModel });
Mock<IScenery> mockScenery = new Mock<IScenery>();
mockScenery.Setup(item => item.IsBlocking(It.IsAny<IModel>(), It.IsAny<IModel>())).Returns(true);
mockGameManager.Setup(item => item.Models).Returns(new List<IModel>() { enemyModel });
mockGameManager.Setup(item => item.SceneryObjects).Returns(new List<IScenery>() { mockScenery.Object });
Model testModel = new Model(2, mockGameManager.Object, mockObjectManager.Object);
testModel.Movement = 4;
testModel.Location.X = 0;
testModel.Location.Y = 0;
testModel.Location.Z = 0;
bool correctExceptionThrown = false;
try
{
testModel.MoveModel(8, 0, 0);
}
catch (MovementException ex)
{
correctExceptionThrown = true;
}
Assert.IsFalse(correctExceptionThrown);
Assert.AreEqual(8, testModel.Location.X);
Assert.AreEqual(0, testModel.Location.Y);
Assert.AreEqual(0, testModel.Location.Z);
}
}

And the code needed in MoveModel itself:

public void MoveModel(float positionX, float positionY, float positionZ)
{
try
{
ValidateMove(ref positionX, ref positionY, ref positionZ);
}
catch (Exception ex)
{
throw;
}
finally
{
this.TotalDistanceMoved += GetDistanceFrom(positionX, positionY, positionZ);
this.Location.X = positionX;
this.Location.Y = positionY;
this.Location.Z = positionZ;
if (this.TotalDistanceMoved > this.Movement)
{
this.IsRunning = true;
}
}
}
private bool ValidateMove(ref float positionX, ref float positionY, ref float positionZ)
{
double distanceToPoint = GetDistanceFrom(positionX, positionY, positionZ);
if (distanceToPoint + this.TotalDistanceMoved > this.Movement * 2)
{
MovementException ex = new MovementException("The model cannot move further than it's Movement rate.");
ex.FinalPosition.X = this.Location.X;
ex.FinalPosition.Y = this.Location.Y;
ex.FinalPosition.Z = this.Location.Z;
throw ex;
}
if (this.TotalDistanceMoved + distanceToPoint > this.Movement)
{
foreach (IModel enemyModel in _gameManager.Models.Where(item => item.Player != this.Player))
{
LocationPoint intersectionPoint = _objectManager.GetPointOfIntersection(this.Location, new LocationPoint(positionX, positionY, positionZ), enemyModel.Location, 8);
if (intersectionPoint != null)
{
if (_objectManager.GetLineOfSight(this, enemyModel) > 0)
{
MovementException ex = new MovementException("The model cannot run within 8\" of an enemy model.");
ex.FinalPosition = intersectionPoint;
positionX = intersectionPoint.X;
positionY = intersectionPoint.Y;
positionZ = intersectionPoint.Z;
throw ex;
}
}
}
}
return true;
}

The lesson I’ve learned from this as far as emergent design goes is that I should be more willing to abstract something out if it doesn’t look like it belongs, and be prepared to program against interfaces that are not implemented yet. I believe that’s the way TDD works, and will keep code exactly where it needs to be and not spread around (as it was starting to do in the Model class…)

Leave a comment

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