Testing TicTacToe

Download Report

Transcript Testing TicTacToe

Notes on Style
Testing the TicTacToe game
26-Jul-16
A first approach


I want to create a large number of tic-tac-toe partial
games for testing purposes
I could do it this way:




char[][] array;
array = new char[][] { { 'X', ' ', 'O' },
{ ' ', 'X', ' ' },
{ 'O', ' ', 'O' } };
...and I could reset the array for each tic-tac-toe board I
want to use as input
This looks nice, and is easy to read, but it is a real
nuisance to type out a lot of these
So I did this instead



setBoard(" o x o o");
private void setBoard(String xxx) {
xxx = xxx.toUpperCase();
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
array[i][j] =
Character.toUpperCase(xxx.charAt(3 * i + j));
}
}
}
Now it's a lot easier to create tic-tac-toe boards for testing
Morals





Use methods to make your life easier
If something is ugly, hide it in a method
Also...
While our main goal should be to write programs that
are easy to read, it isn’t our only goal
The best thing to do with hard-to-read methods is to
rewrite them


Second best is to explain them in comments
I didn’t include the comments on the slide, but they are in
my code!
Refactoring


Refactoring is reorganizing a program without changing
what it does
Refactor in order to:


Make a program easier to understand
Make a program easier to modify
Before refactoring

public final void testMakeCornerMove() {
setBoard(" oxoxxoxo");
computerPlayer.makeMove(board);
assertBoardIs("xoxoxxoxo");
setBoard("oo xxooxx");
computerPlayer.makeMove(board);
assertBoardIs("ooxxxooxx");
}

setBoard("oxoxxoxo ");
computerPlayer.makeMove(board);
assertBoardIs("oxoxxoxox");
I seem to be doing the same thing over and over...
After refactoring


private void beforeAndAfterMove(String before, String after) {
setBoard(before);
computerPlayer.makeMove(board);
assertBoardIs(after);
}
public final void testMakeCornerMove() {
// Center and all other corners taken
beforeAndAfterMove(" o x o o", "x o x o o");
beforeAndAfterMove("o x o o", "o x x o o");
beforeAndAfterMove("o o x o ", "o o x o x");
beforeAndAfterMove("o o x o", "o o x x o");
// Corner move is all that's left
beforeAndAfterMove(" oxoxxoxo", "xoxoxxoxo");
beforeAndAfterMove("oo xxooxx", "ooxxxooxx");
beforeAndAfterMove("oxoxxoxo ", "oxoxxoxox");
beforeAndAfterMove("xxooxxxoo", "xxooxxxoo");
}
Moral

The DRY principle: Don’t Repeat Yourself

Every piece of data should have a single unique representation



“A man with a watch knows what time it is. A man with two watches is
never sure.” -- Segal’s Law
Example: If you have a measure of distance, don’t keep it in two
variables, distanceInFeet and distanceInMeters -- keep it in one
variable, and use a method to convert to the other units as needed
Each nontrivial operation should be represented by a unique
piece of code



Don’t “cut and paste” code--turn it into a method
Variations in code can often be handled by a parameter list
Corrections and updates are much simpler
Testing for a winning move

Here’s one way to test for a winning move:


There are 24 combinations to test for


if (board.get(1, 1) == 'X' && board.get(1, 2) == 'X'
&& board.get(1, 3) == ' ') {
board.set(1, 3, 'X');
return true;
}
This is why I made testing for a winning move optional!
Using a method would help some


if (winningMove(1, 1, 1, 2, 1, 3)) return true;
But that’s still 24 error-prone lines, plus a method
A bright idea

For each location on the tic-tac-toe board,


Put an 'X' in that location
Check for a win (with our computerHasWon() method)



If it’s a win, that’s our move
Otherwise, put a blank in that location, and keep trying
We can do something very similar for testing if we need
to make a blocking move
The code


private boolean makeWinningMove(TicTacToeBoard board) {
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (!board.isEmpty(i, j)) continue;
board.set(i, j, 'X');
if (board.computerHasWon()) return true;
board.set(i, j, ' ');
}
}
return false;
}
This code works, but...
An unexpected consequence



Row
Row
Row
Row
Row
Row
Row
Row
Row
Row
Row
1,
1,
2,
2,
2,
1,
2,
1,
2,
2,
3,
column 1
column 2
column 1
column 1
column 3
column 2
column 1
column 2
column 1
column 3
column 2
is
is
is
is
is
is
is
is
is
is
is
already
already
already
already
already
already
already
already
already
already
already
taken.
taken.
taken.
taken.
taken.
taken.
taken.
taken.
taken.
taken.
taken.
Why did this happen?
I did check for a blank space before placing my 'X'
In the set method of TicTacToeBoard




if (board[row - 1][column - 1] != ' ') {
error("Row " + row + ", column " + column +
" is already taken.");
}
I can only “set” a location if it is initially blank
I never thought about “erasing” an X or an O
Proposed solution: Modify the set() method


Problem: I asked you not to modify the provided methods
Under these constraints, my “bright idea” cannot be
made to work :-(
Morals


Insofar as possible, methods should do a single thing
In particular, it’s usually a bad idea to mix computation
and input/output in the same method


If you mix computation and input/output in the same method,
then you can’t do the computation without also doing the
input/output
Example: In a previous assignment I specified methods
findDayOfWeek to only do computation, and
findAndPrintDayOfWeek to call the former and print the
results

This allowed me to test your computations without getting a bunch of
output
Fix #1 for board.set(row, column, ch)




I could have made set return a boolean--true if the location was
set, false if it wasn’t
boolean set(int row, int column, char ch) {
if (board[row - 1][column - 1] == ' ' &&
(ch == 'X' || ch == 'O')) {
board[row – 1][column – 1] = ch;
return true;
}
else return false;
}
Disadvantage: The user might not check the result
Disadvantage: I test for two things that could go wrong (location
is taken, bad character) and this doesn’t distinguish between them
Fix#2 for board.set(row, column, ch)



I could assert that the location is available, and assert
that the character is legal
void set(int row, int column, char ch) {
assert board[row - 1][column - 1] == ' ';
assert ch == 'X' || 'O' ;
board[row – 1][column – 1] = ch;
}
Disadvantage: Bad use of assert--it should be used for
things you believe to be true, not for error checking
Fix#3 and #4

I could throw an Exception for each error condition



This is the best solution
We haven't covered Exceptions yet
(I nearly forgot this one) I could just skip error checking

Big disadvantage: No warning to the user if something is
wrong
The End