I've been squinting at TDD as a development concept for a while now. I'm coming from a background where all you did was testing after the fact (if you were lucky and had a patient boss), and I remember the pain of extending or changing code in such an environment. So TDD looked like a great concept to me, and now I've decided to put it to the test.
My plan is as follows: I'm going to create a project with only the vaguest of notions as to where it may be going, and see just how useful TDD is going to be in helping me create maintainable code. As an added obstacle, I have to say that I don't know much about TDD other than the principle of red-green-refactor.
So this exercise should be interesting.
Here's the concept of the software I'm going to create: A piece of software that creates a world that a player can walk through and do stuff. How's that for a fuzzy brief?
If I'm going to have a world that a player can navigate, it stands to reason that I need locations, and that my locations have to connect to other locations. And in order for a player to get to other locations, each location needs exits, which shouldn't point anywhere before they've been assigned. I'll also need to be able to get and set the location that an exit leads to, and each location should have a unique ID, which can be assigned to an exit in another location.
Here are the tests I've come up with:
[TestMethod]
public void Location_is_constructed_with_correct_Id()
{
Location location = new Location(1);
Assert.AreEqual(1, location.Id);
}
[TestMethod]
public void Location_is_constructed_with_all_exits_set_to_no_exit()
{
Location location = new Location(1);
Assert.AreEqual(-1, location.ExitInDirection(0));
Assert.AreEqual(-1, location.ExitInDirection(1));
Assert.AreEqual(-1, location.ExitInDirection(2));
Assert.AreEqual(-1, location.ExitInDirection(3));
}
I'm not entirely sure about the second test because, strictly speaking, it tests two things at the same time (the constructor and the ExitInDirection
method), but I don't want my method of storing exits to be public
, so until I figure out how to access private
variables in a unit test, this'll have to do.
I implement the constructor and ExitInDirection
method to return hard-coded values other than the expected test results in order to get my first reds, then flesh them out as follows, which gives me green tests:
private static int _numberOfDirections = 4;
private int[] _exits;
public int Id { set; get; }
public Location(int id)
{
Id = id;
_exits = new int[_numberOfDirections];
for (int i = 0; i < _numberOfDirections; i++)
{
_exits[i] = -1;
}
}
public int ExitInDirection(int direction)
{
return _exits[direction];
}
I don't think I want to keep the number of possible exits to be hard-coded in the Location
class, but that's for later. For now, I can't see any other tests that I could apply to the constructor, but the ExitInDirection
method is another thing. I need to test that against a direction that's outside of the bounds of my array of exits.
[TestMethod]
public void Location_returns_no_exit_if_direction_is_negative()
{
Location location = new Location(1);
Assert.AreEqual(-1, location.ExitInDirection(-1));
}
[TestMethod]
public void Location_returns_no_exit_if_direction_is_greater_than_available_directions()
{
Location location = new Location(1);
Assert.AreEqual(-1, location.ExitInDirection(4));
}
With the current code, both those tests are red as they throw an out of bounds exception, so now I refactor to account for invalid directions:
public int ExitInDirection(int direction)
{
if (direction >= _numberOfDirections || direction < 0)
return -1;
return _exits[direction];
}
And with that amendment, all my tests are now green. Looking good so far.
Another thing I need is a method to determine where each exit direction leads to. But so far I can't set the location for an exit, so that's my next goal.
First, I'll create a method to link a location to an exit, which initially does nothing at all:
public void SetExitToLocation(int direction, int destinationLocationId)
{
}
And some tests to check whether an exit links to the correct location after it's been created:
[TestMethod]
public void CreateExitToLocation_stores_id_of_target_location_in_correct_direction()
{
Location location = new Location(1);
Location destination1 = new Location(1);
Location destination2 = new Location(2);
Location destination3 = new Location(3);
location.SetExitToLocation(1, destination1.Id);
location.SetExitToLocation(2, destination2.Id);
location.SetExitToLocation(3, destination3.Id);
Assert.AreEqual(-1, location.ExitInDirection(0));
Assert.AreEqual(1, location.ExitInDirection(1));
Assert.AreEqual(2, location.ExitInDirection(2));
Assert.AreEqual(3, location.ExitInDirection(3));
}
Running this against the blank method gives me my initial red, so now I'm good to flesh out my SetExitToLocation
method:
public void SetExitToLocation(int direction, int destinationLocationId)
{
_exits[direction] = destinationLocationId;
}
At this point, I have to ask myself again what should happen if the direction is outside of the bounds of the array. Right now, I get an index out of bounds exception, which isn't all that bad, but I figure that an argument out of range exception would make more sense. So I add two more tests for that:
[TestMethod]
public void CreateOneWayExitToLocation_throws_exception_if_direction_is_negative()
{
Location location = new Location(1);
Assert.ThrowsException<ArgumentOutOfRangeException>(() => location.SetExitToLocation(-1, 1));
}
[TestMethod]
public void CreateOneWayExitToLocation_throws_exception_if_direction_is_greater_than_available_directions()
{
Location location = new Location(1);
Assert.ThrowsException<ArgumentOutOfRangeException>(() => location.SetExitToLocation(10, 1));
}
This gives me another red test until I amend my SetExitToLocation
method to:
public void SetExitToLocation(int direction, int destinationLocationId)
{
if (direction >= _numberOfDirections || direction < 0)
throw new ArgumentOutOfRangeException("direction", "Value exceeds the limit of the array");
else
_exits[direction] = destinationLocationId;
}
And that has my whole range of tests green again, and completes my first draft for my location
class.
P.S. Since I'm new to TDD, any constructive comments as to what could be improved in my process are very welcome.