Páginas

sábado, 17 de agosto de 2019

Entender Test Driven Development (TDD) con un ejemplo (parte 2 de 2)


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.

jueves, 8 de agosto de 2019

Entender Test Driven Development (TDD) con un ejemplo (Parte 1 de 2)


Leyendo el libro Test-Driven Java Development de Viktor Farcic y Alex García publicado por Packt Publishing en 2015, he encontrado un ejemplo a modo de Kata (demostración sencilla de la técnica) que ilustra muy bien cuáles son las ventajas de esta forma de construir código.

Para comprender este post, es necesario tener conocimientos básicos de: qué es TDD, un lenguaje de programación orientado a objetos (aquí se usa java) y entender que es un marco de pruebas unitarias XUnit (aquí se usa JUnit).

Una de las piezas clave para hacer un buen desarrollo TDD es tener la capacidad de desgranar la funcionalidad a implementar en muy pequeñas partes. Esta capacidad de idear partes mínimas a programar en cada ciclo Green-Red-Refactor es imprescindible si queremos ciclos cortos, a ser posible de minutos, tal y como recomienda la técnica. En este texto los autores presentan el ejemplo que procedo a explicar y que según mi criterio muestra la técnica a la perfección.

Ejemplo: Tres en raya

Tres en raya es un juego en el que sobre un tablero de 3x3 casillas, 2 jugadores juegan por turnos rellenando una casilla cada vez. Gana el jugador que consigue tener 3 casillas rellenas alineadas en horizontal, en vertical o en diagonal. 

El objetivo del ejemplo es implementar el código necesario para jugar a 3 en raya. Este es el requisito que el programador recibe. El desarrollo debe comenzar por algún trozo de código que se pueda probar y desarrollar preferiblemente en minutos. En este ejemplo se propone empezar por:

1.    Primer ciclo red-green-refactor

 Definir los límites que en horizontal no puede sobrepasar una pieza. Es decir la pieza solo puede estar en las casillas 1, 2 o 3.

Para empezar se hace un caso de prueba para comprobar que la pieza no se intenta colocar fuera del rango, provocando una excepción en este caso. Para detectar la excepción en la prueba con JUnit recurrimos a la anotación @Rule.

    @Rule
    public ExpectedException exception =
            ExpectedException.none();
    private TicTacToe ticTacToe;

    @Test
    public void whenXOutsideBoardThenRuntimeException()  {
        exception.expect(RuntimeException.class);
        ticTacToe = new Tictactoe();
        ticTacToe.play(5, 2);
    } 

Esta es la fase red del ciclo TDD, puesto que la ejecución de la prueba no es satisfactoria ya que ni siquiera existe la clase Tic-tac-toe. Ahora se crea esta clase.

public class TicTacToe {

    public void play (int x, int y) {
        if (x < 1 || x > 3) {
            throw
            new RuntimeException("X is outside board");
        }
   }

}

Ahora se pasa a la fase green, la ejecución de la prueba ya es satisfactoria.
En la fase refactor, modificamos la prueba introduciendo la anotación @Before para crear el objeto tic-tac-toe. Esto se hace porque van a existir más pruebas y estas también necesitarán crear un objeto de esta clase.

    @Rule
    public ExpectedException exception =
            ExpectedException.none();
    private TicTacToe ticTacToe;

    @Before
    public final void before() {
        ticTacToe = new TicTacToe();
    }

    @Test
    public void whenXOutsideBoardThenRuntimeException()  {
        exception.expect(RuntimeException.class);
        ticTacToe.play(5, 2);
    } 

De esta forma se ha completado un ciclo red-green-refactor. ¿Cuánto tiempo puede llevar este trabajo de codificación y pruebas? Seguramente no más allá de minutos.

2.    Segundo ciclo red-green-refactor

 El siguiente ciclo red-green-refactor se dedica a la funcionalidad para las casillas verticales. Se siguen los mismos pasos, lo primero el caso de prueba.

    @Test
    public void whenYOutsideBoardThenRuntimeException() {
        exception.expect(RuntimeException.class);
        ticTacToe.play(2, 5);
    }

Estamos en fase red no hay tratamiento para el valor de Y. Importante la prueba de X sigue funcionando. Se añade el tratamiento para Y.

public class TicTacToe {

    public void play (int x, int y) {
        if (x < 1 || x > 3) {
            throw
            new RuntimeException("X is outside board");
        } else if (y < 1 || y > 3) {
            throw
            new RuntimeException("Y is outside board");
        }
   }

Estamos en fase green. En este momento se repasa el código y se decide no refactorizar nada. Es decir no hay nada que mejorar.

3.    Tercer Ciclo red-green-refactor

La casilla en la que se coloca la pieza no puede estar ocupada. Primero la prueba.

    @Test
    public void whenOccupiedThenRuntimeException() {
        ticTacToe.play(2, 1);
        exception.expect(RuntimeException.class);
        ticTacToe.play(2, 1);
    }

Después de fase red la implementación.

public class TicTacToe {

    private Character[][] board = {{'\0', '\0', '\0'},
            {'\0', '\0', '\0'}, {'\0', '\0', '\0'}};

    public void play (int x, int y) {
        if (x < 1 || x > 3) {
            throw
            new RuntimeException("X is outside board");
        } else if (y < 1 || y > 3) {
            throw
            new RuntimeException("X is outside board");
        }
        if (board[x - 1][y - 1] != '\0') {
            throw
            new RuntimeException("Box is occupied");
        } else {
            board[x - 1][y - 1] = 'X';
        }
    }

}

Después de fase green la refactorización. En refactorización se decide reorganizar la clase para mejorar la legibilidad del código.

public class TicTacToe {

    public void play(int x, int y) {
        checkAxis(x);
        checkAxis(y);
        setBox(x, y);
    }

    private void checkAxis(int axis) {
        if (axis < 1 || axis > 3) {
            throw
                    new RuntimeException("X is outside board");
        }
    }

    private void setBox(int x, int y) {
        if (board[x - 1][y - 1] != '\0') {
            throw
                    new RuntimeException("Box is occupied");
        } else {
            board[x - 1][y - 1] = 'X';
        }
    }
}

Se comprueba que la prueba sigue terminando con éxito después de la refactorización. Todo preparado para continuar con el siguiente ciclo hasta que se termine de implementar el juego tres en raya al completo.

Conclusión

Este ejemplo muestra cómo mediante TDD vamos construyendo un código que además de tener pruebas unitarias automatizadas, está refactorizado no introduciendo deuda técnica. Esta forma de desarrollo también dota al código de un diseño que poco acoplado y muy cohesivo.

Si el ejemplo te está resultando interesante puedes ver la continuación en el post “ Entender Test Driven Development (TDD) con un ejemplo (Parte 2 de 2)” en este mismo blog.

Soto de Real, 8 de agosto de 2019.
Madrid.
España.