BDD y TDD en el mundo real (I) – Metodologías y herramientas

El Desarrollo guiado por pruebas (TDD: Test Driven Development)  y el Desarrollo guiado por comportamiento (BDD: Behavior Driven Development) son metodologías complementarias que tienen como objetivo asegurar la calidad del software desde la fuente.

A diferencia de lo que algunos entienden, BDD no es un reemplazo de TDD. si no que son enfoques de calidad en distintos niveles del producto o servicio a desarrollar.

Este es el primer post de una serie enfocada a entender las distintas metodologías y herramientas para aplicar y automatizar el testing en distintos escenarios.

TDD

TDD está enfocado y proviene del mundo de las pruebas unitarias. Está optimizado para pequeñas piezas de código; pequeños incrementos de funcionalidad.

TDD propone un proceso iterativo en el cual el desarrollo está guiado por los test. Primero escribimos los test que expresan los requerimientos a cumplir luego desarrollamos para cumplir con dichos requerimientos.

TDD propone el siguiente proceso:

  • Test-First: las pruebas se escriben antes del propio código a probar.
  • Automatización: las pruebas del programa deben ser hechas en código, y con la sola ejecución del código de pruebas debemos saber si lo que estamos probando funciona bien o mal.
  • Refactorización posterior: para mantener la calidad del diseño, se cambia el diseño sin cambiar la funcionalidad.

En la práctica se implementan las siguientes acciones:

  • Se definen todos los tests de la unidad a crear.Se toma un test
    • Se verifica que el test falla
    • Se escribe el código para pasar el test
    • Se verifica que el test pasa
    • Se refactoriza el código para eliminar la duplicación
  • Repetir el proceso hasta completar los tests

Caso práctico

Tomaré un caso práctico sencillo del libro de Kent Beck: Test-Driven Development By Example.

En este escenario se plantea un sistema que genera un informe en multi-divisa tal como se representa en la siguiente tabla:

InstrumentoAccionesPrecioTotal
IBM100025 USD25000 USD
Novartis400150CHF40000 CHF
  Total:75000 USD

Asociado a las divisas está un ratio de cambio:

DeACambio
CHFUSD1.5

¿Qué comportamiento necesitaremos para producir un informe validado? Dicho de otra manera, ¿cuál es el conjunto de pruebas que, una vez aprobadas, demostrarán la presencia de un código que estamos seguros calculará correctamente el informe?

  • Necesitamos poder agregar cantidades en dos divisas diferentes y convertir el resultado dado un conjunto de tasas de cambio.
  • Necesitamos poder multiplicar un monto (precio por acción) por un número (número de acciones) y recibir un monto.

Haremos una lista de tareas (TO-DO) para recordarnos lo que tenemos que testar, mantenernos enfocados y evidenciarlo cuándo hayamos terminado:

TO-DO

  • $5 + 10CHF = $10 si CHF:USD es 2:1
  • $5 * 2 = $10

1.     Agregar el test

¿Qué objeto necesitamos primero? Pregunta capciosa. En TDD no empezamos con objetos, empezamos con las pruebas!

Intentémoslo de nuevo: ¿Qué prueba necesitamos primero?

Mirando la lista, esa primera prueba parece complicada. Comencemos por poco o nada:

  • La multiplicación

Cuando escribimos una prueba, imaginamos la interfaz perfecta para nuestra operación. Nos estamos contando una historia sobre cómo se verá la operación desde el exterior.

Nuestra historia no siempre se hará realidad, pero es mejor comenzar desde la mejor API posible y trabajar hacia atrás que hacer cosas complicadas, feas y “realistas” desde el principio.

Aquí hay un ejemplo simple de multiplicación [en JUnit]:

public void testMultiplication() {

          Dollar five = new Dollar(5);

          five.times(2);

          assertEquals(10, five.amount);

}

2.      Verificar que el test falla

La prueba que acabamos de escribir ni siquiera compila.

Eso es bastante fácil de arreglar.

¿Qué es lo menos que podemos hacer para compilar, incluso si no se ejecuta? Tenemos cuatro errores de compilación:

  • Sin clase “Dollar”
  • Sin constructor
  • Sin método “times(int)”
  • Sin campo “amount”

Tomemos los errores uno a la vez.

Podemos deshacernos del primer error definiendo la clase Dólar:

public class Dollar {

}

Ahora necesitamos el constructor, pero no tiene que hacer nada, solo es necesario que compile la prueba:

public class Dollar {

          Dollar (int cantidad){         

     }

Necesitamos una implementación de times(). De nuevo, haremos el menor trabajo posible solo para que la prueba se compile:

public class Dollar {

     Dollar (int cantidad){         

     }

     void times(int multiplier){

     }

}

Finalmente, necesitamos un campo de cantidad:

public class Dollar {

     int amount;

     Dollar (int cantidad){         

     }

     void times(int multiplier){

     }

}

Ahora podemos ejecutar la prueba y ver cómo falla.

Estamos viendo la temida barra roja. Nuestro framework de prueba (JUnit, en este caso) ha ejecutado el pequeño fragmento de código con el que comenzamos, y notó que aunque esperábamos “10” como resultado, tuvimos “0”.

Tristeza? No no. El fracaso es progreso. Ahora tenemos una medida concreta de fracaso.

Eso es mejor que solo saber vagamente que estamos fallando.

3.      Escribir el código para pasar el test

Nuestro problema de programación se ha transformado de “darme multi-monedas” a “hacer que esta prueba funcione, y luego hacer que el resto de las pruebas funcionen“. Mucho más simple. Con un alcance mucho menor.

Podemos hacer que esta prueba funcione. Probablemente no os va a gustar la solución, pero el objetivo en este momento (en cada paso) no es obtener la respuesta perfecta, el objetivo es pasar la prueba. Es la propuesta de TDD.

Haremos nuestro sacrificio en el altar de la verdad y la belleza más tarde.

Este es el cambio más pequeño que podría imaginar que haría que nuestra prueba pase:

public class Dollar {

int amount=10;

Dollar (int cantidad){

}

void times(int multiplier){

}

}

4.      Pasar el test

¡Ahora tenemos la barra verde!

 

5.      Refactorizar para eliminar la duplicación

Obviamente la anterior no es la solución final. Estamos en una primera iteración de pruebas. Ahora introduciríamos refactorizaciones para mejorar y generalizar la solución.

Pero… ¿dónde está la duplicación? Usualmente vemos duplicación entre dos trozos de código.

Aquí la duplicación es entre los datos en la prueba y los datos en el código. No se ve? ¿Qué tal si escribimos?

int amount= 5 * 2;

Ese “10” tendría que venir de algún lado. Hicimos la multiplicación en nuestras cabezas tan rápido que ni siquiera nos dimos cuenta. El “5” y “2” están ahora en dos lugares, y debemos eliminar implacablemente la duplicación antes de seguir adelante

No hay un solo paso que eliminemos el 5 y 2. Sin embargo, ¿qué pasa si movemos el ajuste de la cantidad desde la inicialización del objeto al método times()?

int amount;

void times(int multiplier) {

amount= 5 * 2;

}

La prueba aún pasa, la barra se mantiene verde!

¿Estos pasos os parecen demasiado pequeños? Recordad, TDD no se trata de tomar pequeños pasos, se trata de poder dar pequeños pasos.

¿Codificaríamos en el día a día con pasos tan pequeños? No. Pero cuando las cosas se ponen un poco raras, nos alegramos de poder hacerlo como en este ejemplo.

Seguimos eliminando la duplicación:¿De dónde podemos obtener un 5? Ese fue el valor pasado al constructor, entonces si lo guardamos en la variable amount:

int amount;

Dollar (int cantidad){

this.amount = cantidad;

}

Podemos usar times():

void times(int multiplier){

amount *=2;

}

El valor del parámetro ” multiplier” es 2, por lo que podemos sustituir el parámetro por la varianble:

public class Dollar {

int amount;

Dollar (int cantidad){

this.amount = cantidad;

}

void times(int multiplier){

amount *=multiplier;

}

}

En cada paso pasamos el test y lo tenemos en verde

¡Ahora podemos tachar el primer test como hecho!

TO-DO

  • $5 + 10CHF = $10 si CHF:USD es 2:1
  • $5 * 2 = $10

A partir de aquí repetiríamos el proceso hasta completar todos los tests.

Mejores prácticas en TDD

  • Escribe el código más simple para pasar la prueba
    • Beneficios: asegura un diseño más limpio y claro; evita características innecesarias
  • Escribe aserciones primero, actuar después
    • Beneficios: aclara el propósito del requisito y una prueba temprana.
  • Minimiza las aserciones en cada prueba
    • Beneficio: evita una ruleta de aserciones; permite la ejecución de más aserciones (en otras pruebas).
  • No introduzcas dependencias entre las pruebas
    • Beneficios: las pruebas funcionan en cualquier orden independientemente de si se ejecuta todo o solo el subconjunto
  • Las pruebas deben correr/ejecutarse rápido
    • Beneficios: las pruebas se usan a menudo
  • Usa mocks
    • Beneficios: reducción de la dependencia del código; ejecución de pruebas más rápida.
  • Usa métodos de configuración y desmontaje
    • Beneficios: permite que la configuración y el desmontaje se ejecuten antes y después de la clase o de cada método de prueba.
  • No uses clases base
    • Beneficios: claridad de la prueba.

Reflexiones finales

Si bien el TDD con pruebas unitarias es una gran práctica, en muchos casos no brinda todas las pruebas que los proyectos necesitan. TDD es rápido de desarrollar, ayuda en el proceso de diseño y da confianza a través de un feedback rápido.

TDD nos ayuda a construir una clase o un módulo. Pero en la vida real, necesitaremos construir escenarios completos.

Como veremos en el siguiente post de esta serie, BDD complementa a TDD en el alto nivel, permitiendo verificar el flujo completo de la aplicación.

Únete a nuestra comunidad

#AlwaysLearning

Formación

  • Sensibilización en la importancia de las e-Competences
  • Capacitación Técnica y en Gestión de la Tecnología
  • Formación a medida
  • Adaptación de contenidos propios a formación presencial y online
Buscar

Solicitar Información

Request Information