Effective Tests: A Test-First Example – Part 3
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 2 of our Test-First example, we continued the implementation of our Tic-tac-toe game using a Test-First approach. This time, we’ll finish out our requirements.
Here’s where we left things:
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 positionit should tell the player the position is occupiedWhen the player attempts to select an invalid positionit should tell the player the position is invalidWhen the game goes firstit should put an X in one of the available positionsWhen 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
Our last two requirements pertain to making the game try to win. The first requirement concerns the game trying to get three in a row while the second pertains to the game trying to keep the opponent from getting three in a row. Let’s get started on the first test:
[TestClass] public class When_the_player_can_not_win_on_the_next_turn { [TestMethod] public void it_should_try_to_get_three_in_a_row() { } }
Let’s assume we’ll be validating that the game gets three in a row by completing a sequence ending with the bottom right position being selected:
[TestMethod]
public void it_should_try_to_get_three_in_a_row()
{
Assert.AreEqual(9, selection);
}
Next, let’s establish a scenario were the bottom right corner should always be the position we would expect the game to choose (as opposed to a scenario where the game might have multiple intelligent moves). The following illustrates a layout where the game has gone first and has already achieved two in a row:
Next, we need to determine how we can force the game into the desired state so we can validate the next position selected. We won’t be able to use the same technique as before, so we’ll need to find a new way of manipulating the state. One way would be to just make the Game’s _layout field public and manipulate it directly, but that would break encapsulation. Another way would be to set the _layout field through reflection, but this would result in our test being more tightly coupled to the implementation details of our Game. To make our Game testable, we need to adapt its interface. If our game relied upon a separate class for choosing the positions, we would then have a seam we could use to influence the layout. Hey … once this is in place we’ll have a way to fix our test coupling problem!
For now, let’s comment out the test we’ve been working on and start on a new test describing how the Game class will interact with this new dependency. Let’s think of the dependency as an “advisor” and describe the interaction as receiving a recommendation by the advisor:
[TestClass] public class When_the_game_selects_a_position { [TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { } }
Next, let’s establish an assertion that validates the selection made by the game. We’ll stick with the same scenario we established earlier, expecting the game to choose the bottom right position:
[TestClass]
public class When_the_game_selects_a_position
{
[TestMethod]
public void it_should_select_the_position_recommended_by_the_advisor()
{
Assert.AreEqual(9, selection);
}
}
Next, we need a way of determining the last position chosen by the game. As we’ve done in our previous tests, we’ll use the single GetPosition() method and a little bit of LINQ goodness to help us out. To figure out what the last move was, we can get a list of all the game positions before and after its next turn. We can then use the two lists to determine which new position was selected:
[TestClass] public class When_the_game_selects_a_position { [TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { IEnumerable<int> beforeLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); // make move here IEnumerable<int> afterLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); int selection = afterLayout.Except(beforeLayout).Single(); Assert.AreEqual(9, selection); } }
Next, let’s establish the Game context along with the call we’re interested in:
[TestClass]
public class When_the_game_selects_a_position
{
[TestMethod]
public void it_should_select_the_position_recommended_by_the_advisor()
{
var game = new Game(advisor);
game.GoFirst();
game.ChoosePosition(1);
IEnumerable<int> beforeLayout = (Enumerable.Range(1, 9)
.Where(position => game.GetPosition(position).Equals('X'))
.Select(position => position)).ToList();
game.ChoosePosition(8);
IEnumerable<int> afterLayout = (Enumerable.Range(1, 9)
.Where(position => game.GetPosition(position).Equals('X'))
.Select(position => position)).ToList();
int selection = afterLayout.Except(beforeLayout).Single();
Assert.AreEqual(9, selection);
}
}
Next, let’s establish our GameAdvisor stub. To get our GameAdvisorStub to recommend the positions we’d like, we’ll pass an array of integers to denote the progression we want the game to use:
[TestMethod] public void it_should_select_the_position_recommended_by_the_advisor() { IGameAdvisor advisor = new GameAdvisorStub(new[] { 3, 6, 9 }); var game = new Game(advisor); game.GoFirst(); game.ChoosePosition(1); IEnumerable<int> beforeLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); game.ChoosePosition(8); IEnumerable<int> afterLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); int selection = afterLayout.Except(beforeLayout).Single(); Assert.AreEqual(9, selection); }
To get our test to compile, we’ll need to create our new IGameAdvisor interface, GameAdvisorStub class and add a constructor to our existing Game class. Let’s start with the advisor types:
public interface IGameAdvisor { } public class GameAdvisorStub : IGameAdvisor { readonly int[] _positions; public GameAdvisorStub(int[] positions) { _positions = positions; } }
Next, let’s create the new constructor for our Game class which takes an IGameAdvisor. We’ll also supply a default no argument constructor to keep our existing tests compiling:
public class Game { public Game() { } public Game(IGameAdvisor advisor) { } ... }
Everything should now compile. Let’s run our tests:
Before we move on, our test could stand a little cleaning up. The verbosity of the LINQ extension method calls we’re using are obscuring the intent of our test a bit. Let’s write a test helper in the form of an extension method to help clarify the intentions of our test:
public static class GameExtensions { public static int GetSelectionAfter(this Game game, Action action) { IEnumerable<int> beforeLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); action(); IEnumerable<int> afterLayout = (Enumerable.Range(1, 9) .Where(position => game.GetPosition(position).Equals('X')) .Select(position => position)).ToList(); return afterLayout.Except(beforeLayout).Single(); } }
Now we can change our test to the following:
[TestClass]
public class When_the_game_selects_a_position
{
[TestMethod]
public void it_should_select_the_position_recommended_by_the_advisor()
{
IGameAdvisor advisor = new GameAdvisorStub(new[] {3, 6, 9});
var game = new Game(advisor);
game.GoFirst();
game.ChoosePosition(1);
int selection = game.GetSelectionAfter(() => game.ChoosePosition(8));
Assert.AreEqual(9, selection);
}
}
Let’s run the test again to make sure it still validates correctly:
Good, now let’s work on making the test pass. Something simple we can do to force our test to pass is to play off of a bit of new information we have at our disposal. Since our new test is the only one using the overloaded constructor, we can use the advisor field as a sort of flag to perform some behavior specific to this test. First, let’s assign the parameter to a field:
readonly IGameAdvisor _advisor; public Game(IGameAdvisor advisor) { _advisor = advisor; }
Next, let’s modify the ChoosePosition() method to set the ninth position to an ‘X’ if the _advisor field is set and the player chooses position 8:
public string ChoosePosition(int position)
{
if( _advisor != null && position == 8 )
{
_layout[8] = 'X';
return string.Empty;
}
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;
}
Now, let’s run our tests:
Now, let’s refactor. To eliminate our fake implementation, let’s start by modifying the Game’s SelectAPositionFor() method to call our new IGameAdvisor field. Well assume the IGameAdvisor supports a SelectAPositionFor() method which allows us to pass in the token and the current layout as a string:
void SelectAPositionFor(Player player)
{
int recommendedPosition =
_advisor.SelectAPositionFor(GetTokenFor(player),
new string(_layout));
_layout[recommendedPosition] = GetTokenFor(player);
}
Next, let’s define the new method on our interface:
public interface IGameAdvisor
{
int SelectAPositionFor(char player, string layout);
}
Next, we need to implement the new method on our stub. To have our stub return the expected positions, we’ll keep track of how many times the method has been called and use that as the offset into the array setup in our test:
public class GameAdvisorStub : IGameAdvisor { readonly int[] _positions; int _count; public GameAdvisorStub(int[] positions) { _positions = positions; } public int SelectAPositionFor(char player, string layout) { return _positions[_count++]; } }
Lastly, we can delete our fake implementation and run the tests:
Oh no, we broke a bunch of tests! They all seem to be failing due to a NullReferenceException. Looking further, this is being caused by the IGameAdvisor field not being assigned when using the default constructor. Let’s fix that by changing our default constructor to call the overloaded constructor with a default implementation of the IGameAdvisor interface:
public Game() : this(new GameAdvisor())
{
}
Next, we’ll create the GameAdvisor class and provide an implementation that mirrors the former behavior:
class GameAdvisor : IGameAdvisor { public int SelectAPositionFor(char player, string layout) { return Enumerable.Range(1, layout.Length) .First(p => layout[p - 1].Equals('\0')); } }
Our Game class works the same way as before, but we now have a new seam we can influence the layout selection with.
We can now turn our attention back to the requirements. Let’s go back and un-comment the test we started with, but this time we’ll use it to drive the behavior of our GameAdvisor:
[TestClass] public class When_the_player_can_not_win_on_the_next_turn { [TestMethod] public void it_should_try_to_get_three_in_a_row() { Assert.AreEqual(9, selection); } }
Next, let’s declare an instance of our GameAdvisor class and ask it to select a position for player ‘X’:
[TestClass] public class When_the_player_can_not_win_on_the_next_turn { [TestMethod] public void it_should_try_to_get_three_in_a_row() { IGameAdvisor advisor = new GameAdvisor(); var selection = advisor.SelectAPositionFor('X', "O\0X\0\0X\0O\0"); Assert.AreEqual(9, selection); } }
Before moving on, let’s consider our initial API. Given any approach, is this really the API we want to work with? While the SelectAPositionFor() method seems like a good start, the parameters feel more like an afterthought than a part of the request. It reads more like a “Do something, and oh by the way, here’s some data”. I didn’t notice when we called it from the Game class, but looking back, that call was aided by the context of its usage. We don’t have any variables telling us what ‘X’ and “O\0A…” mean.
One of the advantages of Test-Driven Development is that it forces us to look at our API from a consumer’s perspective. When we build things from the inside out, we often don’t consider how intuitive the components will be to work with by our consumers. Once we’ve started implementation, our perspective can be prejudiced by our understanding of how the system works. TDD helps to address this issue by forcing us to consider how the components will be used. This in turn guides us to adapt the design to how the system is being used rather than the other way around. Let’s see if we can improve upon this a bit:
[TestClass] public class When_the_player_can_not_win_on_the_next_turn { [TestMethod] public void it_should_try_to_get_three_in_a_row() { IGameAdvisor advisor = new GameAdvisor(); var selection = advisor.WithLayout("O\0X\0\0X\0O\0").SelectBestMoveForPlayer('X');</b> Assert.AreEqual(9, selection); } }
That seems to express how I’d like to interact with our advisor more clearly. This breaks our code though, so we’ll need to make some adjustments to our IGameAdvisor interface and GameAdvisor class:
public interface IGameAdvisor { int SelectBestMoveForPlayer(char player); IGameAdvisor WithLayout(string layout); } class GameAdvisor : IGameAdvisor { string _layout; public int SelectBestMoveForPlayer(char player) { return Enumerable.Range(1, _layout.Length) .First(p => _layout[p - 1].Equals('\0')); } public IGameAdvisor WithLayout(string layout) { _layout = layout; return this; } }
That would work, but this implementation would allow us to call the advisor without specifying the layout. Let’s take just a little more time to clear that up by moving the SelectBestMoveForPlayer() method to an inner class to prevent it from being called directly:
public interface IGameAdvisor { IPositionSelector WithLayout(string layout); } public interface IPositionSelector { int SelectBestMoveForPlayer(char player); } class GameAdvisor : IGameAdvisor { public IPositionSelector WithLayout(string layout) { return new PositionSelector(layout); } class PositionSelector : IPositionSelector { readonly string _layout; public PositionSelector(string layout) { _layout = layout; } public int SelectBestMoveForPlayer(char player) { return Enumerable.Range(1, _layout.Length) .First(p => _layout[p - 1].Equals('\0')); } } }
Next, let’s fix up our GameAdvisorStub:
public class GameAdvisorStub : IGameAdvisor
{
readonly int[] _positions;
int _count;
public GameAdvisorStub(int[] positions)
{
_positions = positions;
}
public IPositionSelector WithLayout(string layout)
{
return new PositionSelector(layout, _positions, _count++);
}
class PositionSelector : IPositionSelector
{
readonly int[] _positions;
readonly int _count;
public PositionSelector(string layout, int[] positions, int count)
{
_positions = positions;
_count = count;
}
public int SelectBestMoveForPlayer(char player)
{
return _positions[_count];
}
}
}
We also need to fix the SelectAPositionFor() method in our Game class:
void SelectAPositionFor(Player player)
{
int recommendedPosition =
_advisor.WithLayout(new string(_layout))
.SelectBestMoveForPlayer(GetTokenFor(player));
_layout[recommendedPosition - 1] = GetTokenFor(player);
}
Now, let’s run our tests and make sure our new test fails for the right reason and that we haven’t broken any of the other behavior:
Only our new test fails, which is what we were hoping for. Now, let’s use the Fake It approach to get our test to pass quickly. Since only one of our tests ever call this method with the token ‘X’ and it doesn’t care about which actual position it is, we can change the GameAdvisor’s PositionAdvisor.SelectBestMoveForPlayer() method to always return 9 for player ‘X’:
public int SelectBestMoveForPlayer(char player) { if (player == 'X') return 9; return Enumerable.Range(0, _layout.Length) .First(p => _layout[p].Equals('\0')) + 1; }
Now, let’s refactor to eliminate our duplication. In order to calculate which position should be selected, we’ll need to know what the winning patterns are and which paths in the layout are closest to the winning patterns. My first thought was that we might be able to reuse the winning pattern regular expressions we defined over in our Game class. Let’s go back and look at that again:
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]..", };
While these patterns define what the winning paths are, I don’t think this will work for our needs because this only matches winning patterns. What we need is a way of examining each of the eight winning paths within the layout to see which is the closest to winning. Let’s start by define a new regular expression that we can use to filter out the paths that can’t win:
class PositionSelector : IPositionSelector
{
readonly Regex _availablePathPattern = new Regex(@"[X\0]{3}");
readonly string _layout;
public PositionSelector(string layout)
{
_layout = layout;
}
public int SelectBestMoveForPlayer(char player)
{
if (player == 'X')
return 9;
return Enumerable.Range(0, _layout.Length)
.First(p => _layout[p].Equals('\0')) + 1;
}
}
This regular expression will match any three characters where each of the characters can be either an ‘X’ or a null. If you’re unfamiliar with regular expressions, the brackets are referred to as Character Classes or Character Sets and allow us to define a group of characters we’re interested in. The curly braces with the number is how we define how many times the pattern should repeat to be a match.
Now, we need to apply this to each of the eight possible paths within our layout. To do so, we’ll need to slice up our layout into the eight possible winning paths. Let’s define an array similar to the one in our Game class, but using the winning positions instead of winning patterns:
class PositionSelector : IPositionSelector
{
readonly Regex _availablePathPattern = new Regex(@"[X\0]{3}");
readonly string _layout;
static readonly int[][] _winningPositions = new[]
{
new[] {1, 2, 3},
new[] {4, 5, 6},
new[] {7, 8, 9},
new[] {1, 4, 7},
new[] {2, 5, 8},
new[] {3, 6, 9},
new[] {1, 5, 9},
new[] {3, 5, 7},
};
public PositionSelector(string layout)
{
_layout = layout;
}
public int SelectBestMoveForPlayer(char player)
{
if (player == 'X')
return 9;
return Enumerable.Range(0, _layout.Length)
.First(p => _layout[p].Equals('\0')) + 1;
}
}
Next, we can loop over each of the _winningPositions, retrieve the slice, compare it to the _availablePathPattern and add the matches to a list of availablePaths:
public int SelectBestMoveForPlayer(char player)
{
var availablePaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => _layout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availablePaths.Add(winningSlice);
}
if (player == 'X')
return 9;
return Enumerable.Range(0, _layout.Length)
.First(p => _layout[p].Equals('\0')) + 1;
}
Now that we have the available paths, we can sort them in descending order based on how many ‘O’s they already have, find the first available slot in the slice and return that as the position:
public int SelectBestMoveForPlayer(char player)
{
var availablePaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => _layout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availablePaths.Add(winningSlice);
}
var bestSlice = availablePaths
.OrderByDescending(path => path
.Count(p => _layout[p - 1] == 'X')).First();
return bestSlice.First(p => _layout[p - 1] == '\0');
}
I think we’re almost done refactoring, but we still have some duplication to eliminate. Both our test and our implementation are using the value of ‘X’ as a constant to represent the player. Let’s fix this by replacing the player’s token to a more neutral value and change our regular expression and sorting call to use the neutral value instead:
readonly Regex _availablePathPattern = new Regex(@"[T\0]{3}"); public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } int[] bestSlice = availablePaths .OrderByDescending(path => path .Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }
Everything should be good to go. Let’s run our tests and see how we did:
Our target test passed, but we broke the test for how the game responds when the player chooses a position that is already occupied. Let’s review the test again:
[TestClass] public class When_the_player_attempts_to_select_an_occupied_position { [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); } }
Because we are choosing the second position in this test, the GameAdvisor avoids recommending positions within the first winning pattern. We could fix this test pretty easily by avoiding positions two or three, but now that we now have a seam to control exactly what positions are selected, let’s use our new GameAdvisorStub to correct this test in a more explicit way:
[TestClass]
public class When_the_player_attempts_to_select_an_occupied_position
{
[TestMethod]
public void it_should_tell_the_player_the_position_is_occupied()
{
var game = new Game(new GameAdvisorStub(new [] {1, 4, 7}));
game.ChoosePosition(2);
string message = game.ChoosePosition(1);
Assert.AreEqual("That spot is taken!", message);
}
}
The last requirement concerns how the game reacts when the player is about to win. Here’s our skeleton:
[TestClass] public class When_the_player_can_win_on_the_next_turn { [TestMethod] public void it_should_block_the_player() { } }
As always, we’ll start by deciding what observable outcome we want to depend upon to know the behavior is working correctly. Since we’re expecting the game to block the player, let’s come up with a scenario we know wouldn’t result from the existing behavior of trying to get three in a row. Let’s say the player goes first and has one position left in the center vertical row to win:
To validate this scenario, we’ll check that the GameAdvisor chooses the eighth position:
[TestClass]
public class When_the_player_can_win_on_the_next_turn
{
[TestMethod]
public void it_should_block_the_player()
{
Assert.AreEqual(8, selection);
}
}
Next, let’s setup the rest of the context to declare the instance of our SUT and establish the layout:
[TestClass]
public class When_the_player_can_win_on_the_next_turn
{
[TestMethod]
public void it_should_block_the_player()
{
IGameAdvisor advisor = new GameAdvisor();
int selection = advisor.WithLayout("\0X\0OX\0\0\0\0").SelectBestMoveForPlayer('O');
Assert.AreEqual(8, selection);
}
}
Now, let’s run our test:
Now, let’s make the test pass. This time, I’ll pass the test by testing specifically for the layout we’re after:
public int SelectBestMoveForPlayer(char player)
{
string layout = _layout.Replace(player, 'T');
var availablePaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => layout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availablePaths.Add(winningSlice);
}
if (layout == "\0X\0TX\0\0\0\0")
{
return 8;
}
int[] bestSlice = availablePaths
.OrderByDescending(path => path
.Count(p => layout[p - 1] == 'T')).First();
return bestSlice.First(p => _layout[p - 1] == '\0');
}
Now, let’s refactor. To get the GameAdvisor to choose the eighth position because it recognizes it’s vulnerable to losing, we’ll need to check how close the player is. To do this, we can find all of the available paths for the player and check if any of them already have two positions occupied. First, we’ll need to know which token the player is using:
public int SelectBestMoveForPlayer(char player)
{
string layout = _layout.Replace(player, 'T');
var availablePaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => layout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availablePaths.Add(winningSlice);
}
char opponentValue = (player == 'X') ? 'O' : 'X';
if (layout == "\0X\0TX\0\0\0\0")
{
return 8;
}
int[] bestSlice = availablePaths
.OrderByDescending(path => path
.Count(p => layout[p - 1] == 'T')).First();
return bestSlice.First(p => _layout[p - 1] == '\0');
}
Next, let’s create a new local layout based on the player’s positions:
public int SelectBestMoveForPlayer(char player)
{
string layout = _layout.Replace(player, 'T');
var availablePaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => layout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availablePaths.Add(winningSlice);
}
char opponentValue = (player == 'X') ? 'O' : 'X';
string playerLayout = _layout.Replace(opponentValue, 'T');
if (layout == "\0X\0TX\0\0\0\0")
{
return 8;
}
int[] bestSlice = availablePaths
.OrderByDescending(path => path
.Count(p => layout[p - 1] == 'T')).First();
return bestSlice.First(p => _layout[p - 1] == '\0');
}
Now, let’s copy the logic we created before and use it to find the available paths for the opponent:
public int SelectBestMoveForPlayer(char player)
{
string layout = _layout.Replace(player, 'T');
var availablePaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => layout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availablePaths.Add(winningSlice);
}
char opponentValue = (player == 'X') ? 'O' : 'X';
string opponentLayout = _layout.Replace(opponentValue, 'T');
List<int[]> availableOpponentPaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => opponentLayout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availableOpponentPaths.Add(winningSlice);
}
if (layout == "\0X\0TX\0\0\0\0")
{
return 8;
}
int[] bestSlice = availablePaths
.OrderByDescending(path => path
.Count(p => layout[p - 1] == 'T')).First();
return bestSlice.First(p => _layout[p - 1] == '\0');
}
Lastly, let’s find all the available paths for which the opponent already has two positions filled and remove our fake implementation:
public int SelectBestMoveForPlayer(char player)
{
string layout = _layout.Replace(player, 'T');
var availablePaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => layout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availablePaths.Add(winningSlice);
}
char opponentValue = (player == 'X') ? 'O' : 'X';
string opponentLayout = _layout.Replace(opponentValue, 'T');
List<int[]> availableOpponentPaths = new List<int[]>();
foreach (var winningSlice in _winningPositions)
{
var slice = new string(winningSlice.ToList()
.Select(p => opponentLayout.ElementAt(p - 1)).ToArray());
if (_availablePathPattern.IsMatch(slice))
availableOpponentPaths.Add(winningSlice);
}
int[] threatingPath = availableOpponentPaths
.Where(path => new string(
path.Select(p => opponentLayout[p - 1]).ToArray())
.Count(c => c == 'T') == 2).FirstOrDefault();
if (threatingPath != null)
{
return threatingPath
.First(position => opponentLayout[position - 1] == '\0');
}
int[] bestSlice = availablePaths.OrderByDescending(
path => path.Count(p => layout[p - 1] == 'T')).First();
return bestSlice.First(p => _layout[p - 1] == '\0');
}
Let’s run our test and see what happens:
Our test still passes, but for some reason we broke the test for testing that the player wins when getting three in a row. Let’s have a look:
[TestClass] public class When_the_player_gets_three_in_a_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 wrote this test to assume we could pick positions without worrying about getting blocked. That behavior has changed, so we’ll need to adapt our test. Again, we can use our new seam to plug in the path we want the Game to follow to stay out of our way:
[TestClass]
public class When_the_player_gets_three_in_a_row
{
[TestMethod]
public void it_should_announce_the_player_as_the_winner()
{
var game = new Game(new GameAdvisorStub(new int[] { 1, 2, 3}));
game.ChoosePosition(4);
game.ChoosePosition(5);
string message = game.ChoosePosition(6);
Assert.AreEqual("Player wins!", message);
}
}
Now that we’ve fixed that test, let’s continue our refactoring effort. In generalizing our code, we introduced some duplication. Let’s fix this by extracting a method for determining the available paths for a given player:
List<int[]> GetAvailablePathsFor(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } return availablePaths; }
Now our SelectBestPlayerFor() method becomes:
public int SelectBestMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = GetAvailablePathsFor(player); char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = GetAvailablePathsFor(opponentValue); int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }
Next, let’s reorganize some of these operations so we can see how things group together:
public int SelectBestMoveForPlayer(char player) { char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = GetAvailablePathsFor(opponentValue); int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } string layout = _layout.Replace(player, 'T'); List<int[]> availablePaths = GetAvailablePathsFor(player); int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }
The first section is all about checking for threating opponent paths, but this isn’t very descriptive at the moment. Let’s move all of that into a method that describes exactly what we’re doing:
public int SelectBestMoveForPlayer(char player) { int? threatingPosition = GetPositionThreateningPlayer(player); if (threatingPosition != null) return threatingPosition.Value; string layout = _layout.Replace(player, 'T'); List<int[]> availablePaths = GetAvailablePathsFor(player); int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); } int? GetPositionThreateningPlayer(char player) { char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = GetAvailablePathsFor(opponentValue); int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } return null; }
Next, let’s extract the code for selecting the next winning path position for the player into a separate method:
public int SelectBestMoveForPlayer(char player) { int? threatingPosition = GetPositionThreateningPlayer(player); if (threatingPosition != null) return threatingPosition.Value; return GetNextWinningMoveForPlayer(player); } int GetNextWinningMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); List<int[]> availablePaths = GetAvailablePathsFor(player); int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); }
Now, we can reduce our SelectBestMoveForPlayer() method down to one fairly descriptive line:
public int SelectBestMoveForPlayer(char player)
{
return GetPositionThreateningPlayer(player) ??
GetNextWinningMoveForPlayer(player);
}
We’re done! Here’s our GameAdvisor implementation:
class GameAdvisor : IGameAdvisor { public IPositionSelector WithLayout(string layout) { return new PositionSelector(layout); } class PositionSelector : IPositionSelector { static readonly int[][] _winningPositions = new[] { new[] {1, 2, 3}, new[] {4, 5, 6}, new[] {7, 8, 9}, new[] {1, 4, 7}, new[] {2, 5, 8}, new[] {3, 6, 9}, new[] {1, 5, 9}, new[] {3, 5, 7}, }; readonly Regex _availablePathPattern = new Regex(@"[T]{3}"); readonly string _layout; public PositionSelector(string layout) { _layout = layout; } public int SelectBestMoveForPlayer(char player) { return GetPositionThreateningPlayer(player) ?? GetNextWinningMoveForPlayer(player); } int GetNextWinningMoveForPlayer(char player) { string layout = _layout.Replace(player, 'T'); List<int[]> availablePaths = GetAvailablePathsFor(player); int[] bestSlice = availablePaths.OrderByDescending( path => path.Count(p => layout[p - 1] == 'T')).First(); return bestSlice.First(p => _layout[p - 1] == '\0'); } int? GetPositionThreateningPlayer(char player) { char opponentValue = (player == 'X') ? 'O' : 'X'; string opponentLayout = _layout.Replace(opponentValue, 'T'); List<int[]> availableOpponentPaths = GetAvailablePathsFor(opponentValue); int[] threatingPath = availableOpponentPaths .Where(path => new string( path.Select(p => opponentLayout[p - 1]).ToArray()) .Count(c => c == 'T') == 2).FirstOrDefault(); if (threatingPath != null) { return threatingPath .First(position => opponentLayout[position - 1] == '\0'); } return null; } List<int[]> GetAvailablePathsFor(char player) { string layout = _layout.Replace(player, 'T'); var availablePaths = new List<int[]>(); foreach (var winningSlice in _winningPositions) { var slice = new string(winningSlice.ToList() .Select(p => layout.ElementAt(p - 1)).ToArray()); if (_availablePathPattern.IsMatch(slice)) availablePaths.Add(winningSlice); } return availablePaths; } } }
While we've been working on our component, another team has been putting together a host application with a nice user interface. We're now ready to hand our component over so it can be integrated into the rest of the application. Afterward, the full application will be passed on to a Quality Assurance Team to receive some acceptance testing. Next time we'll take a look at any issues that come out of the integration and QA testing processes.