Páginas

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.




martes, 28 de mayo de 2019

Por qué adoptar prácticas DevOps


Hoy en día independientemente del tipo de negocio en que una empresa esté  involucrada, la dependencia del software es enorme. La forma de construir y entregar  software dicta en gran medida el éxito o fracaso del negocio en cuestión.
Para la puesta en marcha de un nuevo software, es necesario por una parte construirlo o como normalmente se llama a esta actividad desarrollarlo. Una vez desarrollado es necesario desplegarlo, es decir instalarlo en los servidores desde los cuales dará servicio y por lo tanto se podrá hacer uso de su funcionalidad. En este escenario no debemos olvidar hablar de la necesidad de mantenimiento del software. Los negocios, las leyes, las normas, las modas, en definitiva el mundo cambia y el software construido ha de ser modificado para adecuarse a las necesidades de cada momento.
Las actividades de desarrollo y despliegue las llevan a cabo distintas personas que tienen conocimientos distintos y complementarios. Hay especialistas en desarrollo y especialistas en despliegue o instalación de software. Los primeros se denominan DEVelopers en inglés y los segundos OPerators. Los developers o profesionales de desarrollo diseñan y construyen software y su objetivo es estar siempre cambiando el software para mejorarlo y crear nuevas funcionalidades, en definitiva estar siempre “cambiando”. Los operators o  profesionales de operaciones por su parte, tienen la misión de mantener el software permanentemente dando servicio, impidiendo que se produzcan fallos y caídas inesperadas, en definitiva mantener la “estabilidad”.
Aunque developers y operators tienen el mismo objetivo final, resulta que sus objetivos individuales suelen entrar en conflicto. Si queremos algo estable (operators), los cambios no son bienvenidos (developers).
DevOps es una forma de dar nombre a un conjunto de buenas prácticas que tienen en cuenta esta dicotomía de intereses e intentan que el trabajo de profesionales de desarrollo y operaciones fluya adecuadamente dando respuesta a las necesidades de software de las organizaciones hoy en día.
Los departamentos de Tecnologías de la Información (TI) de las organizaciones son responsables de responder con rapidez a los cambios del software, cambios muy necesarios en un mundo cómo el actual muy competitivo y proporcionar servicios seguros, estables y de confianza a los clientes.
Según “DevOps Handbook” de Gene Kim y Jez Humble en TI se produce una espiral de destrucción debido a que: primero los profesionales de operaciones están intentando mantener estables sistemas cuya infraestructura es compleja,  frágil y en muchos casos está mal documentada, segundo alguien en la organización (marketing, comercial, …) promete grandes funcionalidades que harán las delicias de los clientes y que van a estar disponibles para ya, tercero todo esto encamina a que las cosas cada vez sean más difíciles, cada profesional de desarrollo y operaciones esté cada vez más ocupado, el trabajo se desestructure, se vuelva todo más complicado y cómo consecuencia la comunicación se vuelve más lenta y la cantidad de trabajo pendiente se vuelve cada vez más grande. Y todo esto empeora y empeora con el paso del tiempo.

Para romper esta espiral, es necesario disponer de medios que permitan dotar de robustez a los sistemas complejos y buscar flujo de trabajo en el ambiente de desarrollo. Esto hará que los profesionales de TI estén entregados a un trabajo en el que el sobreesfuerzo y la heroicidad no tengan que ser una constante, es decir un entorno normalmente “amable” de trabajo.

Existen evidencias de que usar prácticas recomendadas por DevOps mejora la construcción y puesta en marcha del software. Una de ellas es el informe “State of DevOps Report” que llevan publicando Jez Humble y Gene Kim desde 2013 anualmente (https://puppet.com/resources/whitepaper/state-of-devops-report ). En las conclusiones del informe de 2018 dice:
Cada año, el State of DevOps report nos enseña algo nuevo. Este año nuestros datos han mostrado que mientras hay muchos caminos individuales de realizar una transformación DevOps, hay formas más rápidas de llegar al éxito. Las organizaciones pueden elegir entre ser sistemáticas en cuanto a cómo evolucionan o pueden adoptar una aproximación más particularizada. Por supuesto, es posible que una aproximación particular funcione, pero lo que vemos entre las organizaciones que han alcanzado los niveles más altos de evolución en DevOps, es que no llegaron ahí por casualidad.
Estamos encantados de ser capaces de proporcionar algo concreto y útil a los equipos que están trabajando duro para mejorar la forma en que trabajan……
Tener en cuenta las prácticas DevOps es crítico para que las organizaciones lidien con la confrontación de intereses entre profesionales de desarrollo y operaciones. Mientras esto no se consiga, los logros de TI serán muy dolorosos y como consecuencia esto hará que las organizaciones que no adopten prácticas DevOps sean cada vez menos competitivas.