sábado, 30 de junio de 2018

Capítulo 31: Green Bar Patterns - TDD By Example

El objetivo del ciclo «red/green/refactor» es, principalmente, estar el menor tiempo posible en «red». Hay que salir de ese estado lo más rápido posible y se puede salir de ese estado realmente rápido aunque a veces el resultado sea un paso transitorio para hacer una refactorización posterior.

Los siguientes patrones se utilizan para salir de estado «red».

Fake it (‘Til You Make It)

La primera implementación cuando se tiene una prueba con error es devolver una constante. De esta manera se sale del estado «red», entramos en «green» y pasamos a «refactor» de manera que gradualmente se transforma la constante en un uso de variables.

En la implementación de xUnit se puede observar que en uno de los primeros pasos se devuelve la cadena como una constante:

Y se va transformando con el uso de variables:

Y más tarde:

Hay dos efectos derivados de estar en «green»:
  • Psicológico: en el estado «green» tenemos la seguridad de que todo se ejecuta correctamente. Se puede refactorizar con confianza.
  • Control del alcance: empezar con un ejemplo concreto e ir generalizando es más fácil que tratar de abarcar todos los cambios de una sola vez.

«Fake It» no vulnera la regla de no escribir código que no es necesario porque en los pasos de refactorización se elimina la duplicación de datos entre la prueba y el código. Por ejemplo:

Como se puede observar la duplicación se encuentra entre la prueba y el código. Se puede evitar cambiando el método «yesterday» para que no esté duplicado.

Se ha cambiado pero sigue habiendo duplicación. Sin embargo, desde que  «MyDate('1.3.02')» es el propio objeto «this» podemos eliminar la duplicación.

No todo el mundo está convencido de algo tan sofisticado por lo que puede utilizar «Triangulate» hasta que uno se canse y empiece a utilizar «Fake It» o incluso «Obvious Implementation».

Triangulate

Se puede hacer las abstracciones de las pruebas de una manera más conservadora si sólo se abstrae cuando se tienen dos o más ejemplos.

Por ejemplo, si queremos hacer una prueba para implementar una función la cual queremos que sume dos números tendríamos:

Si estamos triangulando entonces tendríamos que escribir otra prueba.

Con esta segunda prueba ya surge la necesidad de abstraer la solución y utilizar las variables.

La triangulación es una herramienta útil en aquellos casos en los que no se está seguro sobre la correcta abstracción. En cualquier otro caso se recomienda usar «Fake It» u «Obvious Implementation».

Obvious Implementation

Tanto «Fake It» como «Triangulation» se centran en el desarrollo en pequeños pasos. Si la implementación es tan sencilla que no necesita de pequeños pasos para realizarse entonces se puede implementar directamente. Esto es la «Obvious implementation».

Es posible que hacer un código que funcione y que a la par sea un código limpio sea demasiado para hacerlo a la vez. Si esto empieza a suceder habría que regresar para solventar la parte de que funcione y, más tarde, el código limpio.

One to Many

Cuando tenemos que implementar una operación que trabaje con colecciones de objetos la estrategia a seguir es implementar primero la operación para una objeto y después hacer que esta funcione con colecciones.

Por ejemplo, supongamos que tenemos que escribir una función que sume un array de números. Primero empezamos sumando un número.

El objetivo es sumar una colección de número así que añadimos un nuevo parámetro a la función para que admita una array de valores.

Este paso es un ejemplo de cómo aislar el cambio. Una vez añadido el parámetro en la prueba se puede cambiar la implementación sin que afecte a la ejecución de la prueba. Ahora podemos cambiar la implementación para  usar el array.

Ahora podemos eliminar el parámetro «value» que inicialmente definimos.

viernes, 29 de junio de 2018

Capítulo 30: Testing Patterns - TDD By Example


Pruebas hijas

Cuando escribimos un caso de prueba tan grande que no se puede continuar con su desarrollo por su dimensión habrá que abordarlo de manera que se escriba un caso de prueba más pequeño que represente parte del caso de prueba más grande. Una vez que el caso de prueba más pequeño está funcionando entonces se reintroduce el caso de prueba más grande.

El ciclo «red/green/refactor» es tan importante para el éxito del desarrollo que cuando se está en riesgo de perderlo merece la pena el esfuerzo de mantenerlo.

Cuando se escribe una prueba demasiado larga hay que tratar de aprender y analizar el porqué ha sido demasiado larga.

Por ejemplo, si en un caso de uso hay que desarrollar tres elementos (A, B y C) podría darse el caso de que desarrollarlas a la vez fuera demasiado complejo. Si se desarrollaran por separado A, B y C el caso de uso sería mucho más sencillo de implementar.

Objetos simulados o Mock objects

¿Como se prueba un objeto que depende de un recurso caro o complicado? Pues creando una versión simulada de ese recurso que contenga datos predefinidos.

Un ejemplo clásico de un recurso para simular es la base de datos. Esta requiere de un tiempo para empezar, es difícil de mantener limpia, etc.

La solución más común es no usar la base de datos real si no escribir en un objeto que actúe como la base de datos pero que esté utilizando realmente la memoria.

Otra ventaja de los objetos simulados, además del rendimiento y de la confiabilidad, es la legibilidad ya que este tendrá definidos los datos que se van a utilizar. Al utilizar una base de datos esta podría contener resultados de otras pruebas y, por ejemplo, una consulta podría devolver 14 elementos y no se sabría realmente porqué tendría que devolver 14 resultados como respuesta correcta.

Los objetos simulados añaden un riesgo ya que podrían no comportarse como lo haría el recurso real. Esto podría solucionarse si este tuviera un conjunto de pruebas para que coincida con los resultado del recurso que se está simulando cuando este esté disponible.

Self-Shunt

En los casos en los que haya que probar si un objeto se comunica correctamente con otro se podría utilizar un objeto simulado («mock object») para comprobar si la comunicación es la correcta. Otra opción es utilizar la propia clase de prueba en vez del objeto simulado o de algún «mock framework».

Por ejemplo, se quiere actualizar de forma dinámica la barra verde de la interfaz de pruebas del usuario. Si se pudiera conectar un objeto «ResultListener» al objeto «TestResult», este podría notificar al primero si la prueba falla, si ha finalizado, etc. Cada vez que se ejecuta una prueba se debe actualizar la interfaz.

La prueba podría escribirse de la siguiente manera.


El objeto que va a contar las notificaciones.


Aunque realmente, si la clase «ResultListener» se está desarrollando como un objeto simulado podemos utilizar la propia clase de la prueba para que haga la misma función.


Las pruebas que están escritas con el patrón «Self Shunt» tienden a leerse mejor. En el ejemplo anterior se puede apreciar que el valor inicial de la cuenta es 0 y después pasa a 1. ¿Cuando se ejecuta el método «startTest()»? Este debe ejecutarse cuando se ejecutan las pruebas.

Es posible que aplicando «Self Shunt» haya que crear algún tipo de interfaz para definir el comportamiento del objeto con el que se quiere comunicar.

Puede darse el caso de que la interfaz vaya añadiendo comportamiento por lo que este comportamiento debe ser implementado en aquellas clases que la utilicen, incluida la clase de pruebas. Esto puede dar lugar a multitud de implementaciones en la clase dejando la clase un poco menos legible.

Trazas de cadenas Log string

En ocasiones tendremos el problema de saber si una secuencia de mensajes se está ejecutando en el orden adecuado. Para ello se puede utilizar un «log» con las cadenas de los mensajes.

Hay un ejemplo de este tipo de estrategia durante el desarrollo de xUnit.

Las cadenas de «log» son especialmente útiles cuando se quiere implementar «Observers» y se esperan una secuencia de notificaciones en un orden determinado. En los casos en los que el orden no sea importante se puede guardar un conjunto de cadenas de manera que se compare el conjunto en la afirmación.

Crash test dummy

Los «crash test dummy» se utilizan como un objeto especial que se encarga de lanzar las excepciones que podrían darse en situaciones excepcionales de error.

Por ejemplo, si queremos probar que ocurre cuando nuestra aplicación quiere guardar un archivo y el disco duro está lleno. El enfoque no consistiría en crear muchos ficheros grandes hasta llenar el disco duro sino en simular el efecto.

El «crash test dummy» podría ser el siguiente.


Por lo que se podría escribir la prueba.


Los objecto Los «crash test dummy» son como un «Mock object» a excepción de que no se necesita simular el objeto entero. Se puede sobreescribir el método que se necesita simular.

Broken Test

Cuando uno está programando en solitario y se termina una sesión de programación se recomienda dejar la última prueba que no se ejecute. De esta manera, cuando se retome el desarrollo en la siguiente sesión hay un punto de partida obvio para empezar y es más fácil retomar el estado mental en el que se dejó la sesión previa.

Clean Check-in

Cuando se está programando en equipo y se termina una sesión, todos las pruebas del equipo deben poder ejecutarse.

Al comenzar una sesión de programación no se sabe cómo habrá cambiado el código por lo que el lugar ideal para empezar es un lugar confianza y certeza. Este lugar es aquel en el que todos las pruebas se pueden ejecutar sin errores.

martes, 26 de junio de 2018

Capítulo 29: Test-Driven Development

Hay algunas preguntas estratégicas básicas que necesitan respuesta antes de poder hablar sobre los detalles de como probar:

    • ¿Qué significa probar?
    • ¿Cuando probar?
    • ¿Cómo elegir qué lógica probar?
    • ¿Cómo elegir qué datos probar?

Probar N

La manera de probar el software es escribir pruebas automatizadas.

Cuando se desarrolla una nueva funcionalidad se pueden probar los cambios pero probar esos cambios no es lo mismo que tener pruebas. Por ejemplo, las pruebas se ejecutan automáticamente mientras que probar se hace de manera manual, por ejemplo buscar un botón en una interfaz.

Las pruebas aportan seguridad cuando se realizan cambios ya que advierten si se ha producido un error. Cuanto más miedo de producir un error más veces habrá que ejecutar las pruebas, de esta manera se reducen el número de errores y además se reduce el estrés de los programadores.

Aislar las pruebas

Las pruebas deberían ser independientes entre sí de manera que el resultado de unas no influya en otras. Se podría dar el caso de que, al compartir algunos datos, unas pruebas dejaran inconsistente el sistema haciendo fallar otra o, lo que es peor, validando las pruebas cuando no debería hacerlo.

Por esta razón las pruebas deberían estar aisladas. Esto conlleva a que el orden de ejecución puede ser independiente.

Las pruebas deberían ejecutarse lo más rápido posible de manera que se puedan ejecutar a menudo.

Listado de pruebas

El listado de pruebas es, tal y como indica, un listado donde se van a incluir todas las pruebas que, inicialmente, hay que hacer.

Esto tiene una ventaja y es que una vez seleccionada la prueba que se quiere desarrollar se puede centrar toda la atención en esa prueba. Si surge una prueba nueva o una nueva tarea, se apunta en el listado para más tarde valorar cuando desarrollarlo.

Así que en la lista se colocan los ejemplos de cada operación que se sabe que se se necesita implementar. Seguido se incluyen aquellas operaciones que no existen todavía pero que se necesitarán en un futuro. Para finalizar se incluyen las refactorizaciones que se piensa que se tendrán que realizar para limpiar el código.

Una vez terminado el listado de pruebas inicial se podría plantear el implementar todas ellas aunque no se recomienda en absoluto. La primera razón es porque con cada prueba que se implementa hay que hacer una refactorización. Si se hacen diez pruebas la refactorización va a ser más complicada. La segunda razón viene dada cuando hay diez pruebas rotas. Es mucho más complejo tratar de conseguir solucionar diez pruebas que una. Hay que tratar de evitar estar en rojo lo menos posible.

Una vez que la prueba se ejecuta correctamente, es posible que la implementación implique nuevas pruebas. Estas deben incluirse al final del listado de pruebas. Lo mismo para las refactorizaciones, como por ejemplo «esto se está viendo feo».

Cuando se termina una sesión de programación hay que tener cuidado de aquellas pruebas que están pendientes y tienen prioridad en la lista. Se pueden usar varias listas, dependiendo de las prioridades, por ejemplo, se puede tener una lista «now» para las pruebas más prioritarias y otra lista «later» para pruebas que lo son menos o no es el momento de abordarlas. Una refactorización de cierta envergadura que no impacte en la prueba que se está desarrollando debería incluirse en la lista «later» para abordarla en un paso posterior.

En aquellos casos en los que se piensa que puede haber una prueba que no está implementado aún y que podría no funcionar entonces esta prueba debería tener prioridad incluso sobre desplegar el código.

Probar primero

¿Cuando habría que escribir la prueba? Siempre se deberían la prueba antes de escribir el código que tiene que probarse.

En los casos en los que de desarrolla la implementación sin realizar las pruebas se empieza a añadir un estrés a la hora de probar de forma manual. A medida que el sistema va añadiendo funcionalidades el estrés va aumentado dado que, posiblemente, no se haya probado lo suficiente.

Para romper ese patrón se puede escribir una prueba antes de escribir el código que tiene que probarse. De esta manera se reduce el estrés de probar.

La ventaja inmediata es para el TDD es que se empieza y se mantiene haciendo bajo un estrés moderado.

Afirmación primero

¿Cuando debería escribirse la afirmación? Debería ser lo primero en escribirse.

Escribir la afirmación primero simplifica el proceso de escribir una prueba. Cuando se escribe una prueba se están resolviendo varios problemas a la vez incluso si no se ha pensado sobre la implementación.

    • ¿Dónde pertenece la funcionalidad? ¿Es una modificación de un método existente, un método nuevo en una clase que ya existe, en una clase nueva?
    • ¿Qué nombres deberían utilizarse?
    • ¿Cómo se va validar la respuesta correcta?
    • ¿Qué es la respuesta correcta?
    • ¿Qué otros pruebas sugiere esta prueba?

Tratar de abordar todos estos problemas a la vez es, cuanto menos, una osadía para la mayoría de la gente por lo que separar el problema en partes más pequeñas es una buena  estrategia. Las preguntas «¿qué es la respuesta correcta?» y «¿cómo se va a validar la respuesta correcta?» son dos problemas que se pueden separar fácilmente del resto y pueden servir como punto de partida para el resto.

Por ejemplo, en el caso de querer comunicarnos con otro sistema a través de un «socket» y que, una vez hecha la comunicación el «socket» debería cerrarse y deberíamos haber leído la cadena «abc».

El primer paso sería escribir las afirmaciones.


Una vez que están escritas, resolvemos el problema de dónde viene «reply» qué será del «socket» claro está.


¿Y «socket»? Este vendrá dado por una conexión a un servidor.


Y antes de esto necesitamos crear un servidor.


De esta manera se crea el guión de la prueba en pequeños pasos.

Datos de prueba

El objetivo de los datos de prueba es que hay que sean fáciles de leer y entender.

Si hay diferencia entre datos entonces estos deberían tener un significado, o sea, que si no hay una diferencia conceptual entre 1 y 2 entonces se debería usar 1.

Si un sistema tiene múltiples entradas de datos y con 3 datos se cubren las distintas posibilidades, o sea, los distintos conceptos entonces no tiene sentido tener un lista de 10 datos como entrada.

Un truco es tratar de que la misma constante signifique más de una cosa.

La alternativa a los datos de prueba son los datos reales y estos son útiles cuando:

    • Se hacen pruebas en sistemas de tiempo real usando trazas de eventos externos.
    • Si se está comparando la salida del sistema actual con la salida de un sistema previo («Parallel Testing»).
    • Si se está refactorizando una simulación y se espera precisamente la misma respuesta cuando ha finalizado, particularmente si la precisión de los números con coma flotante puede ser un problema.

Datos evidentes

Los datos que se utilizan en la prueba deben representar su intención de una manera clara. Las pruebas se escriben para personas.

El valor esperado y el valor actual deben tener relación aparente. Por ejemplo, en el caso de hacer un cambio de una moneda a otra con una tasa de cambio por parte del banco de un 1,5%, si cambiamos 100USD a GBP y el cambio está a 2:1 entonces el resultado debería ser 50GBP-1,5% = 49.25GBP.


En este caso aparece el número mágico 49.25, que podemos evitar haciendo los cálculos.



En este ejemplo se puede observar cómo se calcula el resultado esperado.

Los datos evidentes tienen el beneficio de hacer la programación más fácil. Una vez que vemos la expresión en la afirmación ya sabemos que tenemos que programar. Además, tenemos la ventaja de poder utilizar datos falsos para poder pasar la prueba e ir evolucionando esta de manera incremental.

Los datos evidentes se pueden ver como una excepción a la regla de los números mágicos. Dentro del alcance de un método la relación entre estos números es obvia. En el caso de tener definidas constantes simbólicas habría que utilizar estas constantes simbólicas.

lunes, 25 de junio de 2018

Capítulo 26: xUnit Retrospective - TDD By Example

Gracias al desarrollo de xUnit que hace Kent Beck podemos apreciar cómo aplica la metodología TDD según su criterio con distintos ejemplos de cómo los pasos pueden variar para albergar más o menos cambios.

El desarrollo de xUnit es una experiencia única para comprender ya no sólo cómo desarrollar un entorno de trabajo de pruebas si no que lo importante son los casos de prueba y cómo llegar a ellos más que la implementación en si misma.

viernes, 22 de junio de 2018

Capítulo 25: How Suite It Is - TDD By Example

Es el momento de abordar la tarea de ejecutar múltiples pruebas sin tener que ejecutarlos de una forma tan poco elegante como se había estado haciendo hasta ahora.

El objetivo es crear una batería de pruebas que pueda ejecutar las distintas pruebas de forma aislada y devolver el resultado de las mismas. Otra razón para implementar «TestSuite» es que nos da un ejemplo de «Composite», esto es que podamos ejecutar una prueba o un grupo de ellas de la misma manera.

Así que, como se hace en TDD comenzamos creando una prueba para después abordar su implementación.

Añadimos la prueba en «main».

Lo ejecutamos y da un error ya que no encuentra la clase «TestSuite». Así que implementamos la clase. En este caso, las pruebas deberían compartir un objeto «TestResult» por lo que lo definimos en el método «run» para que lo utilice cada prueba.

Sin embargo, el patrón «composite» establece que la colección de pruebas tiene que responder los mismos mensajes que una prueba. Así que, si añadimos un parámetro al método «run()» de los «TestCase» tenemos que añadir el mismo parámetro al método «run()» de «TestSuite». Así que se va a utilizar el patrón «Collecting Parameter» y se crearán los «TestResult» en las clases donde se hace la llamada.

Esta solución tiene la ventaja de que el método «run()» no devuelve un resultado sino que altera el estado del objeto «result». Modificamos las clases «TestSuite» para que acepte el parámetro.

Modificamos la clase «TestCase» para pasar el parámetro.

Y ahora podemos modificar la ejecución de las pruebas en «main».

Si ejecutamos «main», podemos observar que fallan el resto de pruebas de «TestCaseTest» ya que no hemos modificado estas para que utilicen el parámetro en su método «result()». Así que pasamos a modificarlas.

Si volvemos a ejecutar «main» no se produce ningún error. Volvemos a estar en «verde».

Podemos darnos cuenta de que «TestResult» se crea en todos los métodos de prueba por lo que podríamos incluir este en el método «setUp()» aunque se añada un poco de complejidad a la hora de leer.

En este capítulo se ha visto:
  • Escribir un método de prueba para «TestSuite». 
  • Escribir parte de la implementación sin hacer funcionar el método de prueba con una implementación falsa («fake implementation»). 
  • Modificar la interfaz del método «run()» para que la prueba y el conjunto de las pruebas funcione de manera idéntica. 
  • Se ha refactorizado la creación de la instancia de «TestResult» en el método «setUp()».

jueves, 21 de junio de 2018

Capítulo 24: Dealing with Faulure - TDD By Example

En este capítulo se continúa con la tarea de informar las pruebas que han fallado:
  • Ejecutar el método de prueba.
  • Ejecutar un «setUp» primero.
  • Ejecutar «tearDown» después.
  • Ejecutar «tearDown» incluso si el método falla.
  • Ejecutar multiples test.
  • Informar de los resultados.
  • Informar de las pruebas que han fallado.
Para abordar esta tarea se va a crear un método de prueba que asegure que se imprime el resultado correcto cuando un método de prueba falla. La clase «TestResult» es la responsable del resultado por lo que se va a implementar un método «testFailed()», de igual manera que «testStarted()», para indicar que la prueba en ejecución ha fallado. El método de prueba sería el siguiente:

Añadimos a «main» su ejecución.

Si lo ejecutamos falla ya que el método «testFailed()» no está implementado. Creamos el método e implementamos el contador «errorCount» como un atributo. El contador se tiene que inicializar a 0.

Si ejecutamos «main» se producirá un error ya que la cadena que devuelve será “1 run, 0 failed” debido a que el método «summary()» tiene una constante.

Si ejecutamos «main» y los método de prueba vuelven a pasar. Estamos en «verde».
Los métodos «testStarted()» y «testFailed()» modifican el texto que va a devolver el método «summary()» dependiendo de si el test empieza o ha fallado. En los casos en los que se ejecute más de un test el uso de estos dos métodos va a modificará cuantos test se han ejecutado y cuántos han fallado.
¿Cuando hay que ejecutar el método «testFailed()»? Cuando el método que se está probando lance una excepción por lo que habrá que capturar si se lanza alguna excepción cuando se ejecuta el método que se está probando.

El problema de esta estrategia es que si hay una excepción durante «setUp()» esta no se captura. Esto se anota para hacer el test más adelante en la lista de tareas:
  • Ejecutar el método de prueba.
  • Ejecutar un «setUp» primero.
  • Ejecutar «tearDown» después.
  • Ejecutar «tearDown» incluso si el método falla.
  • Ejecutar multiples test.
  • Informar de los resultados.
  • Informar de las pruebas que han fallado.
  • Capturar e informar de los errores de «setUp»
En este capítulo se ha visto:
  • Cómo crear un test para un caso particular.
  • Cómo implementar la solución para un caso general con el uso de variables.
  • Anotar el control de errores en el método «setUp()» para hacerlo en una fase posterior y no abordarlo inmediatamente.

martes, 19 de junio de 2018

Capítulo 23: Counting - TDD By Example

Según la lista de tareas la siguiente sería ejecutar «tearDown» incluso si el método falla:
  • Ejecutar el método de prueba.
  • Ejecutar un «setUp» primero. 
  • Ejecutar «tearDown» después. 
  • Ejecutar «tearDown» incluso si el método falla. 
  • Ejecutar multiples test. 
  • Informar de los resultados.
Hay que asegurar que «tearDown()» se ejecuta independientemente de si hay algún tipo de excepción durante la prueba lo que implica el control de las excepciones que puedan ocurrir.  Controlar las excepciones también implica que, en el caso de producirse un error, la excepción se capturará y no se informará de ella.

En general, el orden de los test es importante y se deberían elegir aquellos test que se tenga la confianza de poder hacerlos funcionar. Aunque se darán casos en los que se resolverá un test pero el siguiente se complicará por lo que habrá que considerar hacer una copia de seguridad y volver un paso atrás. Así que, en este caso, se va a realizar la tarea de informar de los resultados.

Cuando se ejecutan todos los test nos gustaría tener un informe con el número de test que se han ejecutado, los que han fallado y la razón por la que han fallado. Aunque esto sería el objetivo final el primer caso sería ejecutar un simple test que no falle.

Para implementar ese informe se va crear un objeto «TestResult» que almacene el resultado de ejecutar el test y este informe será devuelto por el test cuando se ejecute.


Si ejecutamos el test falla. Ya tenemos el test para empezar a desarrollar. Así que empezamos con la implementación más sencilla que es crear la clase y devolver en el método «summary()» la cadena esperada.

Se devuelve una instancia de la clase «TestResult» en el método «run()» de «TestCase».

Si ejecutamos los test estos vuelven a funcionar. Estamos en verde. Toca la fase de refactorización. Se procede a cambiar la implementación de «summary()» en pequeños pasos. Primero se cambian el número de test  que se han ejecutado para que se coja desde una variable.


Los test funcionan correctamente. Seguimos refactorizando de lo particular a lo general. Los test no deberían empezar en 1 si no en 0. Entonces tendremos que inicializar desde 0 la variable e incrementarla desde un método «testStarted()» a la cual se hará una llamada cada vez que se ejecute un test.

¿Dónde se tiene que hacer la llamada a «testStarted()»? La llamada se tiene que hacer antes de la llamada «setUp()» en el método «run()».

Se podría cambiar la constante 0 del número de test fallados para que fuera una variable de la misma manera que se ha cambiado «runCount()». Sin embargo, como no es necesario para el test no se hace. Lo que sí se hace en estos casos es crear un nuevo test para demandar el cambio. Para ello creamos un nuevo método de prueba «testFailedResult()».

Creamos un nuevo test en «main» y lo ejecutamos.
Si ejecutamos este test efectivamente falla por no estar implementado el método «testBrokenMethod()». Así que hacemos la implementación y lanzamos una excepción.

Al ejecutar nuevamente los test tenemos un error con la excepción la excepción que acabamos de lanzar la cual no hemos controlado. Aunque nos gustaría controlar la excepción lo dejamos sin implementar..
  • Ejecutar el método de prueba. 
  • Ejecutar un «setUp» primero. 
  • Ejecutar «tearDown» después. 
  • Ejecutar «tearDown» incluso si el método falla. 
  • Ejecutar multiples test. 
  • Informar de los resultados. 
  • Informar de las pruebas que han fallado.

En este capítulo se ha visto:

  • Escribir una implementación simulada (fake implementation) para ir refactorizando hacia el mundo real reemplazando las constantes con variables. 
  • Se ha escrito otro test. 
  • Cuando un test falla se ha escrito otro test, a un escala más pequeña, para dar soporte haciendo que los test que fallan funcionen.

lunes, 18 de junio de 2018

Capítulo 22: Cleaning Up After - TDD by Example

Según la lista de tareas la siguiente sería «ejecutar “tearDown”»:
  • Ejecutar el método de prueba.
  • Ejecutar un «setUp» primero. 
  • Ejecutar «tearDown» después.
  • Ejecutar «tearDown» incluso si el método falla.
  • Ejecutar multiples test.
  • Informar de los resultados.

Hay veces que los test necesitarán de recursos externos que estarán definidos en «setUp()». Si los test tienen que ser independientes los recursos tienen liberarse en algún momento antes de terminar el test y para ello se va a utilizar el método «tearDown()».

Sería lógico que según la estrategia de «flags» que estamos utilizado hasta ahora se necesite un otro «flag» para saber si un recurso se ha liberado. El incremento de «flags» empieza a ser un poco molesto y, además, hay otro aspecto que no se está teniendo en cuenta y es el orden de llamada. El método «setUp()» debe llamarse antes de «tearDown».

Si en vez de utilizar los «flags» se utiliza un pequeño «log» al cual se vayan añadiendo textos con el nombre de los métodos que se van ejecutando podríamos verificar qué métodos se han llamado y en qué orden.

Para realizar la refactorización primero implementamos la variable «log» y en añadimos al log el texto «setUp».


Refactorizamos el test «testSepUp()» para verificar que el texto que hay en el log coincide con la cadena «setUp ».


Ejecutamos «main» y se ejecuta correctamente. Ahora se puede borrar el flag «wasSetUp» de la clase «WasRun».


Se vuelven a ejecutar «main» y funciona todo correctamente.

El siguiente paso es refactorizar para cambiar el método de prueba «testSetUp()» y que utilice el log. Como el método «setUp()» se ejecuta primero el resultado del log tiene que ser «setUp testMethod ».


Ejecutamos «main» y, efectivamente, falla el test. Ahora refactorizamos para añadir la cadena «testMethod » al «log» cuando se ejecuta un test.



Ejecutamos «main» y Upss! Con este cambio se rompe el método de prueba «testSetUp» ya que el valor del log es «setUp testMethod » y no «setUp ». Así que cambiamos el texto por el resultado esperado.



Ejecutamos «main» y no se producen errores.

Este método de prueba «testSetUp()» incluye tanto la verificación de si se ha ejecutado «wasRun» tanto com si se ha ejecutado el «setUp». Por esta razón, se pueden refactorizar los dos métodos de prueba de manera que «testSepUp» se renombra a «testTemplateMethod()» y «testRunning()» ya no es necesario.



Se puede apreciar que la instancia «WasRun» sólo se está utilizando en el método «testTemplateMethod» por lo que es una buena práctica refactorizarla e incluirla en el único método que la utiliza.


Kent Beck comenta que hacer una refactorización basada en un par de usos para más tarde deshacerla es una práctica común. Hay personas que prefieren esperar hasta que tienen tres o cuatro usos antes de refactorizar por no deshacer trabajo pero Kent prefiere gastar ese tiempo en el diseño y hacer las refactorizaciones en vez de preocuparse si tendrá o no que deshacer la refactorización en un paso posterior.

Una vez hecha la refactorización es tiempo de implementar «tearDown()». Dado que tenemos un método de prueba que está basado en la cadena del log, vamos a modificar este para incluir la cadena «tearDown ».
 


Ejecutamos «main» y tenemos un error. Estamos en «rojo» y tenemos que desarrollar la funcionalidad para volver a «verde».

Como queremo que «tearDown()» se ejecute después de la llamada al método de prueba lo más lógico es hacer la llamada después de que el método de prueba se haya ejecutado. Para ello creamos un método «tearDown()» y hacemos la llamada dentro del método «run()».



Aunque hayamos definido el método «tearDown()», es la clase «WasRun» quien lo sobrecarga


La ejecución ya no da error y no hace falta refactorizar ya que el código funciona y es sencillo.

En este capítulo:

  • Se ha cambiado la estrategia de testeo de flags a un log. 
  • Se ha implementado «tearDown» usando el log.

Podemos tachar la tarea «tearDown» de la lista:

  • Ejecutar el método de prueba. 
  • Ejecutar un «setUp» primero. 
  • Ejecutar «tearDown» después. 
  • Ejecutar «tearDown» incluso si el método falla. 
  • Ejecutar multiples test. 
  • Informar de los resultados.

miércoles, 6 de junio de 2018

Capítulo 21: Set the table - TDD By Example


Cuando se están escribiendo test, estos tienen una estructura en común (Bill Wake la denominó estructura 3A):

    • Arrange: crear algunos objetos.
    • Act: estimular estos objetos.
    • Assert: verificar el resultado.

En este punto se plantean dos posibles problemas:

    • El rendimiento: los test deberían ejecutarse tan rápido como sea posible y la creación de objetos requiere un cierto tiempo. Es posible crear objetos que sean comunes a los distintos test.
    • El aislamiento: los test deberían ser independientes unos de otros.

El acoplamiento de test se produce cuando el resultado de un test depende de otro test. Esto hay que evitarlo ya que los test deberían ser independientes estar aislados.

Asumiendo que el rendimiento no es actualmente un problema, la estrategia debería centrarse en el  aislamiento de los test.

Revisamos la lista de tareas para saber con qué tarea continuar:


    • Ejecutar el método de prueba.
    • Ejecutar «setUp» primero.
    • Ejecutar «tearDown» después.
    • Ejecutar «tearDown» incluso si el método falla.
    • Ejecutar multiples test.
    • Informar de los resultados.

En este capítulo se aborda la ejecución de un método «setUp» antes de la ejecución del método de prueba. El enfoque para desarrollar esta funcionalidad va a ser similar al de la ejecución del método de prueba ya que para saber si el método «setUp» se ha ejecutado vamos a utilizar un nuevo indicador o «flag»: «wasSetUp».

Para realizar la implementación vamos a partir de un ejemplo específico y en lugar de sacar valores por la consola se va a incluir una aserción («assert») para verificar que está funcionando correctamente.


En este punto Kent Beck entiende esto como una prueba que debería estar dentro de un método de prueba. Así que se crea la clase «TestCaseTest», que extiende de «TestCase» e implementa el método «testSetUp». Este método de prueba contiene la creación de una instancia «WasRun», su ejecución y la verificación de que «wasSetUp» tiene el valor correcto a través de una aserción de tipo «assert». Es una prueba que contiene a su vez otra prueba (como diría Tony Stark: «¿y por qué no?»).


Añadimos la prueba a «main» para su ejecución:


Si lo ejecutamos obtenemos un error ya que no está definido el atributo «wasSetUp». Así que vamos a definir el atributo «wasSetUp» y un método para asignar el valor en la clase «WasRun».


Si ejecutamos el script «main» obtenemos una excepción lanzada por el método «assert» del método de prueba. Así que sólo tendríamos que hacer la llamada al método «setUp» para asignar el valor esperado por «assert», o sea, «true».

¿Cuándo debería hacerse la llamada al método «setUp()»? Esta llamada debería hacerse siempre justo antes de ejecutar un método de prueba. Los métodos de prueba se ejecutan en el método «run()» de la clase padre «TestCase» por lo que este es el lugar para hacer la llamada. También hay que definir la función «setUp()» en «TestCase» para que funcione correctamente:


Si se ejecuta «main» no se produce ningún error.

En este momento podemos refactorizar así que se puede mover la inicialización del atributo «wasRun» al método «setUp» de manera que se puede prescindir del constructor de la clase y este se simplifica.


Si se ejecuta funciona correctamente.

También se puede crear un método de prueba para comprobar si un método se ha ejecutado. Se va a crear un test dentro de «TestCaseTest» para comprobar «wasRun».


Se crea el objeto y la llamada en el archivo «main».


Si ejecutamos «main» funciona correctamente.

Se pueden simplificar los métodos de prueba ya que ambos crean una instancia de «WasRun».Esta instancia se puede crear durante el método de «setUp()» y usarla en los métodos de prueba. Como cada prueba crea una instancia al inicio de su ejecución los métodos de prueba no estarían acoplados entre sí (salvo que haya alguna variable global o similar).

Así que creamos un atributo «test» en «TestCaseTest» y sobrecargamos el método «setUp()» para crear una nueva instancia de «WasRun».


En este capítulo se ha visto: 

    • La simplicidad de escribir las pruebas prima sobre el rendimiento de las mismas de ahí que cada test corra su propia instancia de «wasRun».
    • Se ha testeado e implementado «setUp()».
    • Se ha simplificado el caso de prueba usando «setUp()».
    • Se ha simplificado los caso de prueba verificando el ejemplo de test case.

Ya podemos tachar de la lista la segunda tarea:
    • Ejecutar el método de prueba.
    • Ejecutar «setUp» primero.
    • Ejecutar «tearDown» después.
    • Ejecutar «tearDown» incluso si el método falla.
    • Ejecutar multiples test.
    • Informar de los resultados.

martes, 5 de junio de 2018

Capítulo 20: xUnit - TDD By Example

En este capítulo Kent Beck propone crear un «framework» de testeo.

La idea básica para este «framework» sería crear un caso de prueba y poder ejecutar un método de prueba de este caso. Esto se podría implementar teniendo una clase «TestCase» en la cual haya un método «run()» que se encargue de ejecutar el método de prueba.

A partir de aquí se plantean una serie de funcionalidades que debería tener el «framework»:

    • Ejecutar el método de prueba.
    • Ejecutar un «setUp» primero.
    • Ejecutar «tearDown» después.
    • Ejecutar «tearDown» incluso si el método falla.
    • Ejecutar multiples test.
    • Informar de los resultados.

La primera tarea a implementar es la de ejecutar el método de prueba. Para abordar esta tarea sería interesante saber si el método de prueba se ejecuta o no. Si tenemos un método de prueba que modifique un indicador («flag») dentro del método de prueba se puede saber con ese «flag» si el método de prueba se ha ejecutado.

Para implementar esta funcionalidad se va a partir de un ejemplo particular y se crea una clase “WasRun”, que es un caso de prueba, con un atributo “wasRun” que es el «flag» qué nos indicará si un método de prueba se ha ejecutado o no.

En este punto, la estrategia va a ser crear un guión («main.php») muy sencillo de cómo tendría que funcionar. El guión podría ser el siguiente:

En el script «main» se está creando una instancia de un caso de prueba («WasRun»), se imprime por pantalla el valor inicial de «wasRun» (el cual debería ser «false» y no imprimir nada), ejecuta el método de prueba «testMethod()» y vuelve a imprimir por pantalla el valor de «wasRun» que en este caso debería tener un valor de «true».

Si se ejecuta el script «main», este no funciona ya que la clase no está creada. El siguiente paso es crear la clase:

Si ejecutamos el script sigue fallando y necesitamos implementar el atributo «wasRun» e inicializarlo.

Si volvemos a ejecutar el script sigue fallando porque aunque se hayan implementado la clase y el atributo todavía quedaría por implementar el método.

Una vez definido el método, ejecutamos el script «main»  y este saca los valores que esperamos por la consola.

Este primer paso del desarrollo se ha basado en llamar al método de prueba de forma estática, es decir, haciendo la llamada al método de prueba «testMethod()» de forma explícita en «main».

Como se indicó en las especificaciones, las pruebas deberían usar la interface «run()» para ejecutar un método de prueba. Ahora es el momento de cambiar la llamada estática por una dinámica.

Para hacerlo dinámico debería inicializarse el método de prueba que se quiere ejecutar y ejecutar el método «run()». Para implementarlo modificamos el «main» de manera que se pase como parámetro en el constructor el nombre del método de prueba que se quiere ejecutar:

Siguiendo la metodología de TDD, se hace el desarrollo mínimo para que el «main» funcione como esperamos. Para conseguirlo, se implementa el constructor al cual añade un parámetro de entrada («methodName») que es el nombre del método de prueba a ejecutar, se implementa el método «run()» y, en su implementación se ejecuta el método «testMethod()» de forma estática (de momento sin utilizar ninguna variable). Este sería un pequeño paso para conseguir que «main» funcione.

Si ejecutamos «main», funciona como esperamos y la salida por consola sigue siendo la misma.

El siguiente paso es generalizar la llamada del método «run()» al método de prueba «testMethod()» usando una variable y no la llamada estática. Para ello se utiliza una llamada a un método variable. 

Al volver a ejecutar «main» se puede comprobar que todo sigue funcionando correctamente.

Esta forma de desarrollo en la que se parte de un ejemplo concreto para más tarde generalizar reemplazando constantes por variables, es un patrón general de refactorización.

Si observamos detenidamente, la clase «WasRun» tiene dos responsabilidades: ejecutar el método de prueba y almacenar si el método de prueba se ha ejecutado. Según el Single Responsibility Principle (SRP) sólo se debería tener una responsabilidad por lo que es el momento de seguir refactorizando.

Para separar las dos responsabilidades se extrae la ejecución del método de prueba a una clase padre «TestCase» que herede la clase «WasRun».


El método «run()» sólo utiliza atributos de la clase padre por lo que es lógico que esté en esa misma clase. Las operaciones deberían siempre estar cerca de los datos.


En cada uno de estos pequeños cambios se ha ido ejecutando el «main» comprobando que todo seguía funcionando correctamente.

El TDD brinda la posibilidad de trabajar en pequeños pasos aunque estos parezcan ridículos. Una vez se va cogiendo soltura con cambios pequeños estos pequeños pasos pueden albergar un conjunto de ellos.

Ya se puede tachar del listado la primera tarea:

    • Ejecutar el método de prueba.
    • Ejecutar «setUp» primero.
    • Ejecutar «tearDown» después.
    • Ejecutar «tearDown» incluso si el método falla.
    • Ejecutar multiples test.
    • Informar de los resultados.

Puedes ver un ejemplo funcionando en mi repositorio tdd by example.