Effective Tests: A Test-First Example – Part 2
Posts In This Series
- Effective Tests: Introduction
- Effective Tests: A Unit Test Example
- Effective Tests: Test First
- Effective Tests: A Test-First Example – Part 1
- Effective Tests: How Faking It Can Help You
- Effective Tests: A Test-First Example – Part 2
- Effective Tests: A Test-First Example – Part 3
- Effective Tests: A Test-First Example – Part 4
- Effective Tests: A Test-First Example – Part 5
- Effective Tests: A Test-First Example – Part 6
- Effective Tests: Test Doubles
- Effective Tests: Double Strategies
- Effective Tests: Auto-mocking Containers
- Effective Tests: Custom Assertions
- Effective Tests: Expected Objects
- Effective Tests: Avoiding Context Obscurity
- Effective Tests: Acceptance Tests
In part 1 of our Test-First example, we discussed the Test-Driven Development philosophy in more detail and started a Test First implementation of a Tic-tac-toe game component.
Here’s the progress we’ve made on our requirements so far:
When the player goes firstit should put their mark in the selected positionit should make the next moveWhen the player gets three in a rowit should announce the player as the winnerWhen the game gets three in a rowit should announce the game as the winnerWhen the player attempts to select an occupied position it should tell the player the position is occupied When the player attempts to select an invalid position it should tell the player the position is invalid When the game goes first it should put an X in one of the available positions When the player can not win on the next turn it should try to get three in a row When the player can win on the next turn it should block the player
Also, here is what our Game class implementation looks like so far:
public class Game { readonly char[] _layout = new char[9]; readonly string[] _winningPatterns = new[] { "[XO][XO][XO]......", "...[XO][XO][XO]...", "......[XO][XO][XO]", "[XO]..[XO]..[XO]..", ".[XO]..[XO]..[XO].", "..[XO]..[XO]..[XO]", "[XO]...[XO]...[XO]", "..[XO].[XO].[XO]..", }; public string ChoosePosition(int position) { _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; } bool WinningPlayerIs(char player) { return _winningPatterns .Any(pattern => Regex.IsMatch(GetLayoutFor(player), pattern)); } string GetLayoutFor(char player) { return new string(_layout.ToList() .Select(c => (c.Equals(player)) ? player : '\0') .ToArray()); } public char GetPosition(int position) { return _layout[position - 1]; } }
Picking up from here, let’s create our next test skeleton:
[TestClass] public class When_the_player_attempts_to_select_an_occupied_position { [TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { } }
Again, we’ll start by determining how we want to validate our requirements. Let’s assume we’ll get a message of “That spot is taken!” if we try to choose a position that’s already occupied:
[TestMethod]
public void it_should_tell_the_player_the_position_is_occupied()
{
Assert.AreEqual("That spot is taken!", message);
}
Since our game is choosing positions sequentially, something easy we can do is to choose the second position, leaving the first open for the game to select. We can then attempt to choose the first position which should result in an error message. I wonder whether depending on the game to behave this way is going to cause any issues in the future though. Let’s move forward with this strategy for now:
[TestMethod] public void it_should_tell_the_player_the_position_is_occupied() { var game = new Game(); game.ChoosePosition(2); string message = game.ChoosePosition(1); Assert.AreEqual("That spot is taken!", message); }
As a reminder, we want to get our test to pass quickly. Since we can do this with an Obvious Implementation of checking if the position already has a value other than null and returning the expected error message, let's do that this time:
public string ChoosePosition(int position) { if(_layout[position -1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; }
It's time to run the tests again:
Our target test passed, but our changes broke one of the previous tests. The failing test was the one that checks that the player wins when getting three in a row. For our context setup, we just selected the first three positions without worrying about whether the positions were occupied or not. This was our third test and at that point we weren’t concerned with how the game was going to determine its moves, but it seems this decision wasn’t without some trade-offs. For now, we can just avoid the first three positions, but I’m starting to wonder if another strategy is in order. Perhaps a solution will reveal itself in time. To avoid the conflict, we’ll select positions from the middle row:
[TestMethod] public void it_should_announce_the_player_as_the_winner() { var game = new Game(); game.ChoosePosition(4); game.ChoosePosition(5); string message = game.ChoosePosition(6); Assert.AreEqual("Player wins!", message); }
We're green again for now. Let's move on to our next test:
[TestClass] public class When_the_player_attempts_to_select_an_invalid_position { [TestMethod] public void it_should_tell_the_player_the_position_is_invalid() { } }
Similar to our previous test, let's assume a message is returned of “That spot is invalid!”:
[TestClass]
public class When_the_player_attempts_to_select_an_invalid_position
{
[TestMethod]
public void it_should_tell_the_player_the_position_is_invalid()
{
Assert.AreEqual("That spot is invalid!", message);
}
}
Now, let's establish a context which should result in this behavior:
[TestClass] public class When_the_player_attempts_to_select_an_invalid_position { [TestMethod] public void it_should_tell_the_player_the_position_is_invalid() { var game = new Game(); string message = game.ChoosePosition(10); Assert.AreEqual("That spot is invalid!", message); } }
Time to run the tests:
The test failed, but not for the right reason. Let's modify the Game class to return an unexpected value to validate our test:
public string ChoosePosition(int position) { if (position == 10) { return string.Empty; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; }
Now we can work on getting the test to pass. We can modify the Game class to check that the position falls within the allowable range about as quickly as we could use a fake implementation, so let's just do that:
public string ChoosePosition(int position) { if (position 9) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; }
Now, let's refactor. While other issues may exist, the only duplication I see right now is that our new error checking duplicates knowledge about the size of the board. Since we need to modify this anyway, let's go ahead and pull this section out into a separate method which describes what our intentions are:
public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = 'X'; int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'O'; if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; } bool IsOutOfRange(int position) { return position _layout.Count(); }
Let's move on to our next test to describe what happens when the game goes first:
[TestClass] public class When_the_game_goes_first { [TestMethod] public void it_should_put_an_X_in_one_of_the_available_positions() { } }
To check that the game puts an 'X' in one of the positions, let's use another enumerable range to check all of the positions for the expected value:
[TestMethod] public void it_should_put_an_X_in_one_of_the_available_positions() { Assert.IsTrue(Enumerable.Range(1, 9) .Any(position => game.GetPosition(position).Equals('X'))); }
Right now, our game only moves after we've chosen a position. We need a way of telling the game to go first, so let's call a method called GoFirst():
[TestMethod] public void it_should_put_an_X_in_one_of_the_available_positions() { var game = new Game(); game.GoFirst(); Assert.IsTrue(Enumerable.Range(1, 9) .Any(position => game.GetPosition(position).Equals('X'))); }
Next, we'll need to add our new method:
public class Game { // ... public void GoFirst() { } }
We're ready to run the tests:
At this point we may have some ideas about how we might implement this, but there isn’t an obvious way I can think of that would only take a few seconds to write, so let’s Fake It again:
public void GoFirst() { _layout[0] = 'X'; }
Refactor time! As a first step, let's copy the code we're using in the ChoosePosition() to find the first available position and use it to assign the value ‘X':
public void GoFirst() { int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = 'X'; }
Next, let's factor out a method to remove the duplication between these two methods:
void SelectAPositionFor(char value) { int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = value; }
Now we can replace the locations in the ChoosePosition() and GoFirst() methods to call our new method:
public string ChoosePosition(int position)
{
if (IsOutOfRange(position))
{
return "That spot is invalid!";
}
if (_layout[position - 1] != '\0')
{
return "That spot is taken!";
}
_layout[position - 1] = 'X';
SelectAPositionFor('O');
if (WinningPlayerIs('X'))
return "Player wins!";
if (WinningPlayerIs('O'))
return "Game wins.";
return string.Empty;
}
public void GoFirst()
{
SelectAPositionFor('X');
}
We now have two places where the game determines what token it's using, so let's fix this. Let's add a new method called GetTokenFor() which will determine whether the game is assigned an ‘X' or an ‘O'. We'll pass it a string of “game”, but we'll just hard-code it to assign ‘X' for now and see where this takes us:
public void GoFirst() { char token = GetTokenFor("game"); SelectAPositionFor(token); } char GetTokenFor(string player) { return 'X'; }
In order for our GetTokenFor() method to assign a token conditionally, it will need some way of figuring out who's going first. If we keep track of the assignments in a dictionary, then this should be fairly straight forward:
Dictionary<string, char> _tokenAssignments = new Dictionary<string, char>(); char GetTokenFor(string player) { var nextToken = (_tokenAssignments.Count == 0) ? 'X' : 'O'; if (_tokenAssignments.ContainsKey(player)) return _tokenAssignments[player]; return _tokenAssignments[player] = nextToken; } }
Next, let’s change the ChoosePosition() method to use our new method instead of the hard-coded assignments:
public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor("player"); SelectAPositionFor(GetTokenFor("game")); if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; }
These changes have introduced some duplication in the form of magic strings, so let's get rid of that. We can define an Enum to identify our players rather than using strings:
public enum Player { Human, Game }
Now we can change our dictionary, the GetTokenFor() method parameter type and the calls to GetTokenFor() from the ChoosePosition() and GoFirst() methods to use the new Enum:
readonly Dictionary<Player, char> _tokenAssignments = new Dictionary<Player, char>(); char GetTokenFor(Player player) { char nextToken = (_tokenAssignments.Count == 0) ? 'X' : 'O'; if (_tokenAssignments.ContainsKey(player)) return _tokenAssignments[player]; return _tokenAssignments[player] = nextToken; } public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(GetTokenFor(Player.Game)); if (WinningPlayerIs('X')) return "Player wins!"; if (WinningPlayerIs('O')) return "Game wins."; return string.Empty; } public void GoFirst() { char token = GetTokenFor(Player.Game); SelectAPositionFor(token); }
Now that we know this works, let’s refactor the rest of the methods that are still relying upon character values to identify the player along with their associated calls:
bool WinningPlayerIs(Player player) { return _winningPatterns .Any(pattern => Regex.IsMatch(GetLayoutFor(player), pattern)); } string GetLayoutFor(Player player) { return new string(_layout.ToList() .Select(c => (c.Equals(GetTokenFor(player))) ? GetTokenFor(player) : '\0') .ToArray()); } void SelectAPositionFor(Player player) { int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = GetTokenFor(player); } public void GoFirst() { SelectAPositionFor(Player.Game); } public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) return "Player wins!"; if (WinningPlayerIs(Player.Game)) return "Game wins."; return string.Empty; }
Here’s what we have so far:
public class Game { readonly char[] _layout = new char[9]; readonly Dictionary<Player, char> _tokenAssignments = new Dictionary<Player, char>(); readonly string[] _winningPatterns = new[] { "[XO][XO][XO]......", "...[XO][XO][XO]...", "......[XO][XO][XO]", "[XO]..[XO]..[XO]..", ".[XO]..[XO]..[XO].", "..[XO]..[XO]..[XO]", "[XO]...[XO]...[XO]", "..[XO].[XO].[XO]..", }; public string ChoosePosition(int position) { if (IsOutOfRange(position)) { return "That spot is invalid!"; } if (_layout[position - 1] != '\0') { return "That spot is taken!"; } _layout[position - 1] = GetTokenFor(Player.Human); SelectAPositionFor(Player.Game); if (WinningPlayerIs(Player.Human)) return "Player wins!"; if (WinningPlayerIs(Player.Game)) return "Game wins."; return string.Empty; } bool IsOutOfRange(int position) { return position < 1 || position > _layout.Count(); } bool WinningPlayerIs(Player player) { return _winningPatterns .Any(pattern => Regex.IsMatch(GetLayoutFor(player), pattern)); } string GetLayoutFor(Player player) { return new string(_layout.ToList() .Select(c => (c.Equals(GetTokenFor(player))) ? GetTokenFor(player) : '\0') .ToArray()); } public char GetPosition(int position) { return _layout[position - 1]; } public void GoFirst() { SelectAPositionFor(Player.Game); } char GetTokenFor(Player player) { char nextToken = (_tokenAssignments.Count == 0) ? 'X' : 'O'; if (_tokenAssignments.ContainsKey(player)) return _tokenAssignments[player]; return _tokenAssignments[player] = nextToken; } void SelectAPositionFor(Player player) { int firstUnoccupied = Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')); _layout[firstUnoccupied] = GetTokenFor(player); } }
We’ve only got two more requirements to go, but we’ll leave things here for now. Next time, we’ll complete our requirements by tackling what looks to be the most interesting portion of our game and perhaps we’ll discover a solution to our coupling woes in the process.