Páginas

miércoles, 28 de agosto de 2019

Kata TDD: Batalla naval sobre los mares de la tierra.


Una Kata en programación es una demostración de una técnica en este caso TDD. Durante la kata el programador experimenta con un ejemplo sencillo si el uso de TDD le resulta práctico y beneficioso para conseguir un código de mejor calidad. La idea de la Kata y el código aquí mostrados están basados en el libro Test-Driven Java Development de Viktor Farcic y Alex García publicado por Packt Publishing en 2015. 

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 TestNG).

Nuestro trabajo es crear un programa que pueda mover barcos alrededor de los mares de la tierra.
Partimos de clases de soporte que ya están construidas y realizan ciertas funcionalidades útiles para mover los barcos. Para obtener el código del que se parte en esta Kata acceder a https://bitbucket.org/vfarcic/tdd-java-ch04-ship.git en el que se encontrará un proyecto gradle. 

Las clases de soporte son: Direction, Location, Planet y Point. Cada una tiene sus correspondientes clases de prueba cuyo nombre es igual al de la clase que prueba con el sufijo Spec.  El objetivo de las clases Spec es realizar las pruebas unitarias de las clases de soporte suministradas y además servir como especificación o documentación de las mismas, por eso se sufijan con Spec de Specification. Además existe la clase Ship que inicialmente está vacía y es donde se va construir el código con su correspondiente clase de prueba o especificación ShipSpec.

Siguiendo la técnica TDD, se va a proceder a realizar ciclos red-gree-refactor abordando las funcionalidades a implementar en lotes de trabajo muy pequeños.

Lote 1

Se necesita conocer la localización del barco para poder moverlo. Además se necesita saber si el barco está mirando hacía: el norte, el sur, el este o el oeste. Se parte de que un barco tiene un punto inicial (x,y) en el que está situado y una dirección hacia la que mira su proa (N, S, E o W).
Entre las clases de soporte está la clase Point que tiene el constructor.

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

En la clase  Direction existe un método Direction. 

public enum Direction {
    NORTH(0, 'N),
    EAST(1, 'E'),
    SOUTH(2, 'S'),
    WEST(3, 'W'),
    NONE(4, 'X');
}

Además hay una clase Location con el siguiente constructor.

    public Location(Point point, Direction direction) {
        this.point = point;
        this.direction = direction;
    }

Usando la técnica TDD se empieza el primer ciclo red-green-refactor construyendo la  prueba para saber dónde está el barco y hacia donde está mirando. 

@Test
public class ShipSpec {
    public void whenInstantiatedThenLocationIsSet() {
        Location location = 
           new Location(new Point(21,13),Direction.NORTH);      
        Ship ship = new Ship(location);      
        assertEquals(ship.getLocation(), location);   
    }

} 

La prueba verifica que el objeto Location que hemos pasado al constructor del barco ha sido almacenado y se puede acceder a él a través del método getLocation() de Ship.
La implementación consiste en: poner un atributo location a los objetos de tipo Ship, un argumento de tipo Location al constructor de Ship y una función getLocation() que devuelva la location en la que se encuentra el barco.

    private final Location location;
    public Location getLocation() {
        return location;
    }


    public Ship(Location location) {
        this.location = location;
    }


En la parte de refactorización del ciclo decidimos añadir una anotación @BeforeMethod para crear una instancia de un objetos de tipo Ship. Esta instancia la necesitaremos en todos los casos de prueba.

@Test
public class ShipSpec {
 
    private Ship ship;
    private Location location;
 
    @BeforeMethod
        public void beforeTest() {         
         Location location = new Location(
             new Point(21, 13), Direction.NORTH);
        ship = new Ship(location);
    }
 
    public void whenInstantiatedThenLocationIsSet() {
          assertEquals(ship.getLocation(), location);
    }
}

Lote 2

Implementar los comandos que muevan el barco adelante (forward) y atrás (backward). La clase de soporte Location tiene los métodos forward y backward, que implementan esta funcionalidad cambiando la localización del barco según se ordene ir hacia delante (forward) o hacia detrás (backward).

Para la prueba. ¿Qué ocurre cuando, por ejemplo, el barco está mirando hacia el norte y se ordena mover el barco hacia adelante? Su localización sobre la coordenada Y debe incrementarse en una unidad. Otro ejemplo sería cuando el barco está mirando hacia el este, debería incrementarse su localización en X en una unidad.

    public void givenNorthWhenMoveForwardThenYIncreases() {
        ship.moveForward();
        assertEquals(ship.getLocation().getPoint().getY(), 13);
    }
 
    public void givenEastWhenMoveForwardThenXIncreases() {
        ship.getLocation().setDirection(Direction.EAST);
        ship.moveForward();
        assertEquals(ship.getLocation().getPoint().getX(), 22);
    }
 
Esta prueba es correcta. Esto se debe a que la prueba requiere tener conocimiento de qué métodos tiene la clase Location. Es decir getPoint() y getX() son métodos de Location y no se deberían usar en la prueba de Ship. Otro problema que tiene este enfoque es que si Location cambia, hay que buscar todos los lugares en los que se usan sus funciones además de en su propia prueba unitaria.
Aquí lo correcto es asumir que el código al que llamamos tiene ya sus propias pruebas. Es decir, Location ya tiene sus propias pruebas unitarias. Estas pruebas se deben ejecutar cada vez que se incorpora o modifica código al proyecto, asegurando que el nuevo código no rompe lo construido (pruebas de regresión).

Una forma más correcta de escribir esta prueba es.

    public void whenMoveForwardThenForward() {
        Location expected = location.copy();
        expected.forward();
        ship.moveForward();
        assertEquals(ship.getLocation(), expected);
    }
 
Cómo Location ya tiene el método forward, todo lo que necesario es asegurar que se invoca al método de la forma adecuada. Para ello, se crea un objeto de la clase Location llamado expected, se invoca al método forward() y se compara la Location expected con la localización del barco después ordenar que se mueva hacia adelante con moveForward().
Ahora se procede la implementación para llegar a la parte green del ciclo. Consiste aquí en codificar en la clase Ship el método moveForward de la siguiente manera

    public boolean moveForward() {
        return location.forward();
    }
 
Para backward realizamos otro ciclo red-green-refactor similar. La prueba.
     public void whenMoveBackwardThenBackward() {
        Location expected = location.copy();
        expected.backward();
        ship.moveBackward();
        assertEquals(ship.getLocation(), expected);
    }
 
La implementación

    public boolean moveBackward() {
        return location.backward();
    }

Lote 3

Implementar los comandos que mueven el barco a derecha e izquierda. Siguiendo el diseño de mover adelante y atrás, realizamos dos ciclos red-green-refactor uno para la izquierda y otro para la derecha. La  prueba para el movimiento hacia la izquierda.

    public void whenTurnLeftThenLeft() {
        Location expected = location.copy();
        expected.turnLeft();
        ship.turnLeft();
        assertEquals(ship.getLocation(), expected);
    }
 
Y la implementación
    public void turnLeft() {
        location.turnLeft();
    }
 
Y para la derecha otro ciclo con la prueba

    public void whenTurnRightThenRight() {
        Location expected = location.copy();
        expected.turnRight();
        ship.turnRight();
        assertEquals(ship.getLocation(), expected);
    }
 
Y la implementación

    public void turnRight() {
        location.turnRight();
    }

Lote 4

El barco puede recibir una cadena de caracteres con comandos. Esto es ‘lrfb’ es equivalente a: left (izquierda), right (derecha), forward (adelante) y backward (atrás). Se empieza por la prueba para el comando f.

    public void whenReceiveCommandsFThenForward() {
        Location expected = location.copy();
        expected.forward();
        ship.receiveCommands("f");
        assertEquals(ship.getLocation(), expected);
    }
 
La implementación

    public void receiveCommands(String commands) {
        if (commands.charAt(0) == 'f') {
            moveForward();
        }
    }
 
La prueba y la implementación es similar para los comandos r, l y b. Teniendo implementado cada comando por separado, ahora vamos a implementar una prueba para probar un comando compuesto ‘rflb’.

    public void whenReceiveCommandsThenAllAreExecuted() {
        Location expected = location.copy();
        expected.turnRight();
        expected.forward();
        expected.turnLeft();
        expected.backward();
        ship.receiveCommands("rflb");
        assertEquals(ship.getLocation(), expected);
    }
 
La implementación la hacemos modificando el método receiveCommands()
    public void receiveCommands(String commands) {
        for (char command : commands.toCharArray()) {
            switch(command) {
                case 'f':
                    moveForward();
                    break;
                case 'b':
                    moveBackward();
                    break;
                case 'l':
                    turnLeft();
                    break;
                case 'r':
                    turnRight();
                    break;
            }
        }
    }

Lote 5

La tierra es una esfera. Cuando nos movemos en un mapa plano llega un momento que no podemos avanzar más pero en una esfera eso no es así, siempre se puede avanzar para cualquier dirección. Lo implementado hasta ahora no tiene esto en cuenta, para tenerlo en cuenta se considera el mapa una plantilla con cuadrícula. De esta forma, cuando se llega a un borde, se pasa al borde contrario para simular el movimiento en la esfera.

La cuadrícula tiene una longitud y anchura máximas. Entonces se va a implementar como llegando al borde se pasa al borde contrario. Se usa la clase de soporte Planet.  Se prepara una prueba en la que al constructor del barco además de enviarle la localización le enviamos planet. El objetivo a la larga es conocer los límites de la cuadrícula del mapa para un determinado planeta. Ahora solo se comprueba que el barco sepa el planeta en el que se encuentra.

    public void whenInstantiatedThenPlanetIsStored() {
        Point max = new Point(50, 50);
        Planet planet = new Planet(max);
        ship = new Ship(location, planet);
        assertEquals(ship.getPlanet(), planet);
    }
 
Para que esto funcione correctamente hay que modificar el constructor de Ship, pero esto puede romper pruebas que ya están usando el constructor solo con el argumento location. Para mantener en funcionamiento lo anterior se crea un nuevo constructor que sobrecarga el anterior.

    public Ship(Location location) {
        this.location = location;
    }
    public Ship(Location location, Planet planet) {
        this.location = location;
        this.planet = planet;
    }
 
Después de comprobar que todas las pruebas siguen funcionando, en la fase de refactoring se valora si dejar un único constructor. Para dejar con un único constructor cambiamos la prueba

public class ShipSpec {
    private Planet planet;
 
    @BeforeMethod
    public void beforeTest() {
        Point max = new Point(50, 50);
        location = new Location(new Point(21, 13), Direction.NORTH);
        planet = new Planet(max);
//        ship = new Ship(location);
        ship = new Ship(location, planet);
    }
    public void whenInstantiatedThenPlanetIsStored() {
//        Point max = new Point(50, 50);
//        Planet planet = new Planet(max);
//        ship = new Ship(location, planet);
        assertEquals(ship.getPlanet(), planet);
    }
}
 
Con esto el constructor de un solo argumento puede ser borrado. Usando esta secuencia de pasos las pruebas permanecen siempre en verde.

Ahora se procede a la implementación del movimiento de una parte de la cuadrícula a otra, es decir el salto al otro lado cuando se llega al límite. Para ello se usan las clases de soporte. El método forward de Location está sobrecargado con el método forward(Point max). Esté método se encarga de llegar a la otra parte de la cuadrícula cuando alcanzamos el límite máximo de cualquier extremo: norte, sur, este y oeste. Se prepara una prueba para comprobar que cuando el barco está mirando hacia el este y está en el límite máximo de la abscisa X si se ordena que avance, vuelva al valor 1 de X, esto simula que da la vuelta en la esfera terrestre.

/* The name of this method has been shortened due to line's length restrictions. The aim of this test is to check the behavior of ship when it is told to overpass the right boundary.
*/
    public void overpassEastBoundary() {
        location.setDirection(Direction.EAST);
        location.getPoint().setX(planet.getMax().getX());
        ship.receiveCommands("f");
        assertEquals(location.getX(), 1);
    }
 
La implementación que corresponde a esto es

    public boolean moveForward() {
//        return location.forward();
        return location.forward(planet.getMax());
    }
 
Ahora se realiza otro ciclo red-green-refactor con el método backward.

Lote 6

En los planetas no todo es agua y por ello es necesario implementar detección de tipo de superficie antes de hacer el movimiento. Si el comando encuentra una superficie no acuática, el barco aborta el movimiento y reporta el obstáculo.

En las clases de apoyo contamos con:
  • Qué el objeto Planet tiene un constructor que acepta una lista de obstáculos. Cada obstáculo es una instancia de la clase Point.
  • Los métodos Location.forward y Location.backward tiene versiones sobrecargadas que aceptan una lista de obstáculos. Estos métodos devuelven true si el movimiento se hizo y falso si falló.
  • Para construir el informe de estado se usa el método Ship.receiveCommands. Este método  debería devolver una cadena de caracteres del estado resultante de la ejecución de cada movimiento. O representa que el movimiento se hiza y X que no se hizo. Ejemplo: OOXO = OK, OK, Failure y OK.
Preparamos la prueba

public void whenReceiveCommandsThenStopOnObstacle() {
        List obstacles = new ArrayList<>();
        obstacles.add(new Point(location.getX() + 1, location.getY()));
        ship.getPlanet().setObstacles(obstacles);
        Location expected = location.copy();
        expected.turnRight();
        // Moving forward would encounter an obstacle
        // expected.forward(new Point(0, 0), new ArrayList());
        expected.turnLeft();
        expected.backward(new Point(0, 0), new ArrayList<>());
        ship.receiveCommands("rflb");
        assertEquals(ship.getLocation(), expected);
    }
    public void whenReceiveCommandsThenOForOkAndXForObstacle() {
        List obstacles = new ArrayList<>();
        obstacles.add(new Point(location.getX() + 1, location.getY()));
        ship.getPlanet().setObstacles(obstacles);
        String status = ship.receiveCommands("rflb");
        assertEquals(status, "OXOO");
    }
 
Y la implementación 

public boolean moveForward() {
//        return location.forward();
        return location.forward(planet.getMax(), planet.getObstacles());
    }
 
    public boolean moveBackward() {
//        return location.backward();
        return location.backward(planet.getMax(), planet.getObstacles());
    }
 
El código sobre el cual se ha realizado la explicación, está en https://bitbucket.org/vfarcic/tdd-java-ch04-ship/branch/req06-obstacles tal y como lo suministran los autores del libro mencionado al comienzo de este post.

Conclusión

Mediante esta Kata se muestra como el código de una aplicación va tomando forma mediante la técnica TDD, en definitiva inicialmente el código no es nada y toma forma dependiendo exclusivamente de las decisiones del programador. Quizás este post le sirva al programador para reflexionar sobre las ventajas que obtendría si usa esta técnica de construcción de código.
En la opinión la autora de este post, se obtienen múltiples ventajas:
  • Pruebas unitarias automatizadas para todo el código
  • Pruebas unitarias bien construidas (principio FIRST)
  • Especificaciones actualizadas
  • Código construido para ser probado
  • Facilidades para el posterior mantenimiento del sistema
  • No hay código superfluo. Solo se codifica lo imprescindible para superar la prueba
Soto del Real. Madrid a 28 de agosto de 2019

martes, 20 de agosto de 2019

Cómo mejorar el canal DevOps. Primeros pasos.

Todas las organizaciones que usan software tienen un canal DevOps. El canal DevOps se define aquí, como el proceso establecido para el control y gestión del ciclo de vida completo del software. El ciclo de vida contempla desde la concepción, el desarrollo y la puesta en marcha, continúa con el mantenimiento y termina con la eliminación. En el camino de este proceso se hacen instalaciones, se realizan pruebas, se usan herramientas software y otras, se realizan tareas manuales, se hacen revisiones, se crean o modifican entornos, se manejan repositorios de código y documentaciones, etc.

Este canal Devops será más o menos caótico, estará documentado o no, funcionará mejor o peor, pero ahí está, todas las organizaciones que usan software lo tienen. Lo que se propone en este post son ideas a tener en cuenta para mejorarlo. Cada instalación es distinta y por esto el canal DevOps no es estándar, pero para conseguir un buen canal es interesante tener en cuenta las prácticas que están funcionando en importantes organizaciones. Aquí se  presentan algunas de estas prácticas de éxito para que puedan ser tenidas en cuenta por aquellos que decidan mejorar el canal DevOps.

Crear las bases para el canal DevOps

Los desarrolladores crean el código en sus puestos de trabajo, aislados de los entornos de pruebas y los productivos. Este código una vez escrito y probado de forma aislada, se integra con el código de los otros miembros del equipo. La integración del código no es cosa sencilla, esta integración supone que el código de varios desarrolladores tiene fundirse en un código único que lleve a cabo la misión para la que ha sido construido. Para facilitar esta integración se usa un repositorio donde todo el código se almacena y donde cada desarrollador acude para integrar lo construido y para descargar código que haya que modificar para mejorarlo o repararlo.  Es muy importante que el repositorio sea único, siendo la fuente de confianza. En el repositorio está depositado el trabajo de todos los miembros del equipo, correctamente integrado.

Es importante también modificar la definición de hecho para que tenga incluido el despliegue y puesta en marcha de los aplicativos en entornos de tipo productivo. Hay que tener en cuenta que un aplicativo no está dando servicio hasta que no está en producción y el canal DevOps no se ha recorrido completo hasta ese momento.

Construir pruebas automáticas que sean rápidas y fiables

Los desarrolladores están continuamente creando código y modificando código existente, es su trabajo. Las pruebas automáticas preservan el código, cuando los desarrolladores lo modifican les sirve para comprobar que todo sigue funcionando correctamente. Al estar automatizadas es fácil ejecutarlas y si están hechas adecuadamente además es rápido. Cada vez que el código se modifica se ejecutan las pruebas para asegurar que no se han introducido errores o disfunciones. También sirven estas pruebas para comprobar que todo va bien cuando el código se despliega en un entorno, o se modifica algún elemento que pueda afectar al funcionamiento. Es interesante tener en cuenta la técnica Test Driven Development (TDD), esta forma de desarrollo asegura que cuando se implementa una funcionalidad, esta tiene una prueba asociada. TDD tiene otras muchas ventajas según sus defensores entre los cuales se encuentran reconocidos expertos.

Otra técnica a tener en cuenta es el Andon Cord. Es un principio de fabricación Lean, promueve la  notificación al equipo y la dirección cuando se produce un problema en el proceso o en la calidad del producto fabricado. La idea es parar la cadena de producción para solucionarlo antes de continuar. En el canal DevOps, esto se puede aplicar a por ejemplo al descubrimiento en el entorno de pre-producción de un código que no cumple las reglas de calidad estática de código. ¿Debemos parar el despliegue? Así evitamos introducir deuda técnica en el aplicativo. Esta es una política que deberiamos tener acordada y a ser posible automatizada.

Promover la integración continua

Está sobradamente demostrado que hacer desarrollos de larga duración que se abordan separando el código de la rama principal del repositorio, no es práctico. Pasado el tiempo los problemas de integración son complicados de resolver sino insalvables. Hay prácticas de desarrollo que establecen como trabajar sin hacer ramas en el repo. Si se decide trabajar con ramas que el tiempo que transcurre trabajando en una rama tienda a ser lo más corto posible.

Conseguir versiones de bajo-riesgo

Esto se refiere a realizar actividades que aseguren que cuando una versión se ponga en funcionamiento, no se produzcan problemas. Una técnica que se puede emplear son los "Dark Launches", esto consiste en poner en producción funcionalidades que se ocultan a los usuarios mediante mecanismos de programación. Estas versiones ya desplegadas pero no utilizadas, permiten poner a prueba su uso por parte de los desarrolladores, antes de liberar su uso para el resto de los usuarios. Cuando definitivamente se libera, hay bastantes garantías de que funcionará.

      Preparar la arquitectura para versiones de bajo riesgo

La arquitectura seleccionada para el sistema debe facilitar la productividad, testabilidad y seguridad. Esto se debe tener en cuenta a la hora de elegir, marcos de desarrollo, bases de datos, etc. teniendo en cuenta las facilidades conque cuentan los equipos que en la arquitectura elegida.

Además en cuanto a la arquitectura, muchas organizaciones comparten sistemas heredados con sistemas de nueva creación. Las arquitecturas de los sistemas heredados tienden a ser monolíticas, mientras que los nuevos tienden a ser orientadas a microservicios. Si se decide mejorar un sistema antiguo por uno nuevo basado en microservicios, tener en cuenta en patrón "Strangler" para poder realizar el cambio poco a poco.

En conclusión      

Mejorar siempre es posible. El canal DevOps es la forma mediante la cual las organizaciones pueden hacer llegar el software que desarrollan a sus usuarios. Es importante hoy en día en un mundo de negociós tan dinámico y cambiante, ser capaces de manener los sistemas al día, tanto modificando su funcionalidad cómo consiguiendo que den servicio 24 horas al día y 365 dias al año. Mejorar el canal DevOps ayudará.

Es conveniente preparar un ciclo periódico de mejora continua (Ciclo de Deming). Por ejemplo anualmente, semestralmente o a demanda. El objetivo es realizar los pasos 1 al 4 de forma repetida lo que implica estar revisando y mejorando de forma constante.

                                                


La fuente de inspiración de este post está en “DevOps Handbook” de Gene Kim y Jez Humble.