I’m inching my way up the TDD ladder and I’ve got to a point where I’d like to get advice on the “next move”. I realize there might not be a single answer here, so any logical suggestion would be great.
The thing I think I’m stuck on is knowing weather I should force duplication or proceed with primary functionality implemented by minimal (and temporary due to not being general) code.
I’m trying to go in a strict green/red/re-factor cycle. Also, this example is so simple it doesn’t require any mocked objects, but I think it’s still a valid example (please correct me if I’m wrong or I’ve gone into some kind of trivial case that’s not worth working with).
UPDATE: I continued with this almost to the conclusion so I suppose I’ll leave it up and see if anyone has any comments and if not I will delete..
Tests:
using NSpec;
namespace TicTacToe
{
public class new_game : nspec
{
protected TicTacToeGame game;
private void before_each()
{
game = new TicTacToeGame();
}
private void when_game_starts()
{
it["board should be clear"] = () => game.BoardState.should_be("---,---,---");
it["it's x's turn"] = () => game.PlayerUp.should_be('x');
it["no winner yet"] = () => game.Winner.should_be('-');
}
}
public class specify_game : new_game
{
private void first_move()
{
context["is x in the center"] = () =>
{
act = () => game.Move('x', 2, 2);
it["board should have x in the center"] = () => game.BoardState.should_be("---,-x-,---");
it["it's y's turn"] = () => game.PlayerUp.should_be('y');
context["then y in the center"] =
() =>
{
it["should throw OtherPlayerOccupiesSpaceException"] =
expect<OtherPlayerOccupiesSpaceException>(() => game.Move('y', 2, 2));
it["no winner yet"] = () => game.Winner.should_be('-');
};
context["then y in the upper left"] = () =>
{
act = () => game.Move('y', 1, 1);
const string expectedBoardState = "y--,-x-,---";
it["correct board state is " + expectedBoardState] = () => game.BoardState.should_be(expectedBoardState);
it["it's x's turn"] = () => game.PlayerUp.should_be('x');
it["no winner yet"] = () => game.Winner.should_be('-');
context["then x in the middle top"] = () =>
{
act = () => game.Move('x', 1, 2);
const string expectedBoardState2 = "yx-,-x-,---";
it["correct board state is " + expectedBoardState2] = () => game.BoardState.should_be(expectedBoardState2);
it["no winner yet"] = () => game.Winner.should_be('-');
context["then y in the right top"] = () =>
{
act = () => game.Move('y', 1, 3);
const string expectedBoardState3 = "yxy,-x-,---";
it["correct board state is " + expectedBoardState3] = () => game.BoardState.should_be(expectedBoardState3);
it["no winner yet"] = () => game.Winner.should_be('-');
context["then x in the middle bottom"] = () =>
{
act = () => game.Move('x', 3, 2);
const string expectedBoardState4 = "yxy,-x-,-x-";
it["correct board state is " + expectedBoardState4] = () => game.BoardState.should_be(expectedBoardState4);
it["x wins"] = () => game.Winner.should_be('x');
};
};
};
};
};
context["is y int the center"] =
() =>
{
it["should throw NotYourTurnException"] =
expect<NotYourTurnException>(() => game.Move('y', 2, 2));
};
}
}
}
Implementation:
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace TicTacToe
{
public class TicTacToeGame
{
private readonly char[,] grid = new char[3,3];
private bool xsTurn = true;
public TicTacToeGame()
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
grid[i, j] = '-';
}
}
}
public string BoardState
{
get
{
var sb = new StringBuilder();
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
sb.Append(grid[i, j]);
}
if (i < 2)
sb.Append(',');
}
return sb.ToString();
}
}
public char PlayerUp
{
get { return xsTurn ? 'x' : 'y'; }
}
public void Move(char player, int down, int over)
{
over -= 1;
down -= 1;
if (player != PlayerUp)
throw new NotYourTurnException();
if (grid[down, over] != '-')
throw new OtherPlayerOccupiesSpaceException();
this.grid[down, over] = player;
xsTurn = !xsTurn;
}
public char Winner
{
get
{
string boardState = BoardState;
var xWins = new[]
{
"xxx,...,...",
"...,xxx,...",
"...,...,xxx",
"x..,x..,x..",
".x.,.x.,.x.",
"x..,x..,x..",
"..x,..x,..x",
"x..,.x.,..x",
"..x,.x.,x..",
};
var yWins = new[]
{
"yyy,...,...",
"...,yyy,...",
"...,...,yyy",
"y..,y..,y..",
".y.,.y.,.y.",
"y..,y..,y..",
"..y,..y,..y",
"y..,.y.,..y",
"..y,.y.,y..",
};
if (xWins.Any(win => Regex.IsMatch(boardState, win)))
return 'x';
if (yWins.Any(win => Regex.IsMatch(boardState, win)))
return 'y';
return '-';
}
}
}
}
Output:
PM> NSpecRunner.exe .TicTacToebinDebugTicTacToe.dll
new game
when game starts
board should be clear
it's x's turn
no winner yet
specify game
first move
is x in the center
board should have x in the center
it's y's turn
then y in the center
should throw OtherPlayerOccupiesSpaceException
no winner yet
then y in the upper left
correct board state is y--,-x-,---
it's x's turn
no winner yet
then x in the middle top
correct board state is yx-,-x-,---
no winner yet
then y in the right top
correct board state is yxy,-x-,---
no winner yet
then x in the middle bottom
correct board state is yxy,-x-,-x-
x wins
is y int the center
should throw NotYourTurnException
17 Examples, 0 Failed, 0 Pending
9
To respond to the comments that you received, I think this was a useful experiment.
To answer your question, the approach that I take is to do the easiest thing to make the test pass. This can have one of two outcomes.
The first outcome is that another test is needed. This happens when the code you’ve written can’t possibly be good enough. The example I always use to demonstrate this is a method that adds two numbers.
Test:
expect(plus(2, 2)).to eq(4)
Implementation:
def plus(x, y)
return 4
end
That’s the simplest way to make the test pass, but it’s obviously not good enough. So we need to add another test so that in order for both tests to pass, the implementation must be correct.
Test:
expect(plus(2, 2)).to eq(4)
expect(plus(2, 3)).to eq(5)
Implementation:
def plus(x, y)
return x + y
end
The second outcome is that I end up copying and pasting, because that’s the easiest way to get a green bar. But that leaves me with duplication that I don’t want. So I refactor in small chunks to remove any duplication and the keep the green bar as I go. I can’t really think of a good small example to demonstrate that, which is something that I should work on.