Este ejemplo empieza en el post “Entender
Test Driven Development (TDD) con un ejemplo (Parte 1 de 2)” que puede ser consultado en este mismo blog.
A continuación se aborda la
funcionalidad referente a determinar el turno de cada jugador. Para esto vamos a preparar 3 pruebas:
- En el primer turno juega X
- Si en el último turno jugó X, entonces en el próximo turno debe jugar O
- Si en el último turno jugó O, entonces en el próximo turno debe jugar X
4. Cuarto ciclo red-green-refactor
El primer turno es para el
jugador X. A esta funcionalidad le corresponde la prueba.
@Test
public void givenFirstTurnWhenNextPlayerThenX() {
assertEquals('X', ticTacToe.nextPlayer());
}
La implementación más sencilla
que le corresponde a esta prueba es.
public
char nextPlayer() {
return 'X';
}
Ya estamos en green sin nada que refactorizar de momento.
5. Quinto ciclo red-green-refactor
Vamos a preparar una prueba para
comprobar que si en el último turno jugó X, ahora juegue Y.
@Test
public void givenLastTurnWasXWhenNextPlayerThenO()
{
ticTacToe.play(1, 1);
assertEquals('O', ticTacToe.nextPlayer());
}
La implementación que le
corresponde a superar esta prueba necesita saber quien jugó la última vez.
private char lastPlayer = '\0';
public void play(int x, int y) {
checkAxis(x);
checkAxis(y);
setBox(x, y);
lastPlayer = nextPlayer();
}
public char nextPlayer() {
if (lastPlayer == 'X') {
return 'O';
}
return 'X';
}
Ahora se plantea la siguiente
prueba, cuando juegue O luego tiene que jugar X. Realmente la implementación
para cumplir con esta prueba ya está hecha. Si se codifica la prueba, se
comprueba que con la implementación realizada la prueba ya está en green. Se
considera por lo tanto que esta prueba no es necesaria.
6. Sexto ciclo red-green-refactor
Acabada la funcionalidad
correspondiente al turno de cada jugador, se aborda la funcionalidad de
determinar si algún jugador ha ganado poniendo todas sus fichas en una línea.
Inicialmente se abordan líneas rectas, horizontales o verticales.
Se prepara la prueba, empezando
por el caso ‘no ganó nadie’.
@Test
public void whenPlayThenNoWinner()
{
String actual = ticTacToe.play(1,1);
assertEquals("No
winner", actual);
}
La implementación más sencilla
para obtener green
public
String play(int x, int y) {
checkAxis(x);
checkAxis(y);
setBox(x, y);
lastPlayer = nextPlayer();
return "No winner";
}
7. Séptimo ciclo red-green-refactor
Ahora se prepara una prueba para
el jugador X ganando con una línea horizontal
@Test
public void whenPlayAndWholeHorizontalLineThenWinner() {
ticTacToe.play(1, 1); // X
ticTacToe.play(1, 2); // O
ticTacToe.play(2, 1); // X
ticTacToe.play(2, 2); // O
String actual = ticTacToe.play(3, 1); // X
assertEquals("X is the winner", actual);
}
Para hacer la implementación que
se corresponde con este test, se tiene que validar si cualquier línea
horizontal se ha rellenado con la marca que corresponde a un jugador. O sea se
necesita saber además de la casillas que están rellenas que jugador ha
rellenado cada una. Entonces la implementación de esta prueba puede quedar de
la siguiente manera:
public String play(int x, int y) {
checkAxis(x);
checkAxis(y);
lastPlayer = nextPlayer();
setBox(x, y, lastPlayer);
for (int index = 0; index < 3; index++) {
if (board[0][index] == lastPlayer
&&
board[1][index] == lastPlayer &&
board[2][index] == lastPlayer) {
return lastPlayer + " is
the winner";
}
}
return "No winner";
}
private void setBox(int x, int y, char lastPlayer){
if (board[x - 1][y - 1] != '\0') {
throw new RuntimeException("Box is occupied");
} else {
board[x - 1][y - 1] = lastPlayer;
}
}
En la fase de refactor se mejora el código creando una función
específica para determinar si algún jugador ha ganado. Se trata de mejorar la
legibilidad del código.
private static final int SIZE = 3;
public String play(int x, int y) {
checkAxis(x);
checkAxis(y);
lastPlayer = nextPlayer();
setBox(x, y, lastPlayer);
if (isWin()) {
return lastPlayer + " is the
winner";
}
return "No winner";
}
private boolean isWin() {
for (int i = 0; i < SIZE; i++) {
if (board[0][i] + board[1][i] + board[2][i]
== (lastPlayer * SIZE)) {
return true;
}
}
return false;
}
Tras refactorizar comprobamos que la prueba sigue estando en green.
8. Octavo ciclo red-green-refactor
También debemos comprobar si los jugadores tienen alguna línea
vertical. Preparamos la prueba.
@Test
public void whenPlayAndWholeVerticalLineThenWinner() {
ticTacToe.play(2, 1); // X
ticTacToe.play(1, 1); // O
ticTacToe.play(3, 1); // X
ticTacToe.play(1, 2); // O
ticTacToe.play(2, 2); // X
String actual = ticTacToe.play(1, 3); // O
assertEquals("O is the winner", actual);
}
La implementación es similar a la anterior pero para las líneas en
vertical.
private boolean isWin() {
int playerTotal = lastPlayer * 3;
for (int i = 0; i < SIZE; i++) {
if (board[0][i] + board[1][i] + board[2][i] == playerTotal) {
return true;
} else if (board[i][0] + board[i][1] + board[i][2] == playerTotal )
{
return true;
}
}
return false;
}
9. Noveno ciclo red-green-refactor
Ahora que tenemos las verticales
y las horizontales es momento de ocuparse de las diagonales. Se empieza por la
diagonal de que va de arriba-izquierda hasta abajo-derecha.
@Test
public void whenPlayAndTopBottomDiagonalLineThenWinner() {
ticTacToe.play(1, 1); // X
ticTacToe.play(1, 2); // O
ticTacToe.play(2, 2); // X
ticTacToe.play(1, 3); // O
String actual = ticTacToe.play(3, 3); // O
assertEquals("X is the winner", actual);
}
La implementación está fuera del
bucle y puede ser
private boolean isWin() {
int playerTotal = lastPlayer * 3;
for (int i = 0; i < SIZE; i++) {
if (board[0][i] + board[1][i] + board[2][i] == playerTotal) {
return true;
} else if (board[i][0] + board[i][1] + board[i][2] == playerTotal ) {
return true;
}
}
if ((board[0][0] + board[1][1] + board[2][2]) == playerTotal) {
return true;
}
return false;
}
10. Décimo ciclo red-green-refactor
Ahora se implementa la prueba
para la diagonal de que va de abajo-izquierda hasta arriba-derecha. La prueba.
@Test
public void whenPlayAndBottomTopDiagonalLineThenWinner() {
ticTacToe.play(1, 3); // X
ticTacToe.play(1, 1); // O
ticTacToe.play(2, 2); // X
ticTacToe.play(1, 2); // O
String actual = ticTacToe.play(3, 1); // O
assertEquals("X is the winner", actual);
}
La implementación.
private boolean isWin() {
int playerTotal = lastPlayer * 3;
for (int i = 0; i < SIZE; i++) {
if (board[0][i] + board[1][i] +
board[2][i] == playerTotal) {
return true;
} else if (board[i][0] +
board[i][1] + board[i][2] == playerTotal ) {
return true;
}
}
if ((board[0][0] + board[1][1] +
board[2][2]) == playerTotal) {
return true;
} else if (playerTotal ==
(board[0][2] + board[1][1] + board[2][0])) {
return true;
}
return false;
}
En refactor se decide mejorar el
tratamiento de las diagonales aprovechando el bucle para recoger el contenido
de las casillas. De esta forma, el
condicional de las diagonales queda más legible.
private
boolean isWin() {
int playerTotal = lastPlayer * 3;
char diagonal1 = '\0';
char diagonal2 = '\0';
for (int i = 0; i < SIZE; i++) {
diagonal1 += board[i][i];
diagonal2 += board[i][SIZE - i - 1];
if (board[0][i] + board[1][i] +
board[2][i] == playerTotal) {
return true;
} else if (board[i][0] +
board[i][1] + board[i][2] == playerTotal ) {
return true;
}
}
if (diagonal1 == playerTotal ||
diagonal2 == playerTotal) {
return true;
}
return false;
}
11. Onceavo ciclo red-green-refactor
Se va a comprobar si se ha
llegado a un empate. Si todas las casillas están llenas y no hay tres casillas
en línea de ningún jugador, hay un empate. Empezamos por la prueba del
empate.
@Test
public void whenAllBoxesAreFilledThenDraw() {
ticTacToe.play(1, 1);
ticTacToe.play(1, 2);
ticTacToe.play(1, 3);
ticTacToe.play(2, 1);
ticTacToe.play(2, 3);
ticTacToe.play(2, 2);
ticTacToe.play(3, 1);
ticTacToe.play(3, 3);
String actual = ticTacToe.play(3, 2);
assertEquals("The result is draw", actual);
}
La implementación.
public String play(int x, int y) {
checkAxis(x);
checkAxis(y);
lastPlayer = nextPlayer();
setBox(x, y, lastPlayer);
if (isWin()) {
return lastPlayer + " is the
winner";
} else if (isDraw()) {
return "The result is
draw";
} else {
return "No winner";
}
}
private boolean isDraw() {
for (int x = 0; x < SIZE; x++) {
for (int y = 0; y < SIZE; y++) {
if (board[x][y] == '\0') {
return
false;
}
}
}
return true;
}
En la fase de refactorización se observa
que la función isWin() puede ser mejorada. No es necesario que cada vez que se
ejecute esta función compruebe todas las casillas, cada vez es suficiente con
que compruebe la casilla rellenada en ese turno para ver si ha completado una
línea horizontal, vertical o diagonal.
private boolean isWin(int x, int y) {
int playerTotal = lastPlayer * 3;
char horizontal, vertical, diagonal1,
diagonal2;
horizontal = vertical = diagonal1 =
diagonal2 = '\0';
for (int i = 0; i < SIZE; i++) {
horizontal += board[i][y - 1];
vertical += board[x - 1][i];
diagonal1 += board[i][i];
diagonal2 += board[i][SIZE - i -
1];
}
if (horizontal == playerTotal
|| vertical == playerTotal
|| diagonal1 == playerTotal
|| diagonal2 == playerTotal) {
return true;
}
return false;
}
Conclusiones
Este ejemplo ilustra la manera en
que se implementan los aplicativos usando la técnica TDD. Cómo se puede
observar a lo largo del ejemplo una de las actividades fundamentales en
desarrollo TDD, es la forma en que se aborda la funcionalidad a implementar
dividiéndola en pequeñas o más bien pequeñísimas partes. Esto es fundamental
para realizar ciclos red-green-refactor que no lleven más allá de minutos. Esto
es lo recomendado por la técnica.
Espero que este ejemplo sea de
utilidad para aquellos desarrolladores que quieren aprender a usar TDD. Según defienden muchos reputados expertos, el
uso de TDD mejora la calidad del trabajo realizado.
Soto de Real, 17 de agosto de 2019
Madrid.
España.
No hay comentarios:
Publicar un comentario