Skip to content

Introducción a BDD – Traducción del original de Dan North

01/07/2013

Historia: El artículo apareció por primera vez en la revista Better Software en Marzo de 2006 y ha sido traducido al japones por Yukei Wachi, al koreanopor HongJoo Lee, al italiano por Arialdo Martini, al Frances por Philippe Poumaroux y ahora al Español por Oscar Zarate. La versión original está en el Blog de Dan North

Introducción a BDD

Siempre que uso y enseño prácticas ágiles, como ser Desarrollo Dirigido por Pruebas (TDD – Test Driven Development), en distintos proyectos y en diferentes entornos, siempre me encuentro con los mismos problemas y mal entendidos. Los programadores quieren saber por dónde empezar, qué testear y qué no testear, cuánto testear en una prueba, a qué llamar “prueba” y cómo saber porque una prueba falla.

Cuanto más avanzo con TDD, más me doy cuenta que mi aprendizaje del mismo ha sido caracterizado por entrar y salir en callejones sin salida. Me he preguntado a mí mismo “¿por qué nadie me dio la solución antes?” muchas más veces que lo que he pensado “se abrió una puerta”. Y siempre pensé que debería existir una forma de presentar TDD en la cual se pueda acceder a lo bueno sin tener que pasar por todos los malos momentos.

Mi respuesta es Desarrollo Dirigido por Comportamientos (BDD – Behaviour Driven Development). BDD es una evolución de las prácticas ágiles y está pensado para hacer estas prácticas ágiles más accesibles y efectivas a los equipos de trabajo que quieren empezar con ellas. Con el paso del tiempo, BDD ha crecido para acompañar el crecimiento de las metodologías ágiles y el uso de pruebas de aceptación automatizadas.

Los nombres de los métodos de prueba deben ser sentencias

La primera vez que “se me prendió la lamparita” fue cuando me estaban mostrando una simple aplicación llamada agiledox, escrita por mi colega Chris Stevenson. Esta herramienta extrae los nombres de las pruebas unitarias de JUnit y las imprime como sentencias. Por ejemplo, en caso como el siguiente

public class BuscadorDeClienteTest extends TestCase {
	testBuscarClienteUsandoId() {
		...
	}
	testFallaCuandoEncuentraDosClientesIguales() {
		...
	}
	...
}

El resultado sería

BuscadorDeClientes
   - Buscar cliente usando Id
   - falla cuando encuentra dos clientes iguales
   - ...

La palabra “test” es eliminada del nombre de la clase y de las pruebas unitarias, como así también se aprovecha el uso de las mayúsculas y minúsculas (anotado de camello –camel case– en inglés) para separar palabras. Eso es todo y el resultado es impresionante.

Los desarrolladores descubrieron que podían obtener algo de documentación y empezaron a denominar sus pruebas unitarias usando sentencias. Y aún más, estos descubrieron que si esos nombres usaban palabras del dominio de negocios, esa documentación podía ser usada por los otros integrantes del equipo (testers, analistas y usuarios finales).

Una simple sentencia genérica ayuda a mantener las pruebas unitarias alineadas

Luego, descubrí lo útil que es empezar los nombres de las pruebas unitarias con la palabra “debería” (should). La sentencia genérica “La clase debería hacer algo” implica que se puede definir una prueba para esa clase. Esto nos mantiene concentrados en la clase. Si te encuentras escribiendo una sentencia que no se adecua a esta sentencia genérica es porque tal vez esa prueba pertenece a otro lugar.

Por ejemplo, si yo estoy escribiendo una clase que validaba el ingreso de datos en una pantalla, donde la mayoría de estos datos son de un cliente, como nombre y apellido, y de repente encuentro que tengo que validar la edad y la fecha de nacimiento. Yo empezaría escribiendo la clase ValidadorDeDetallesDelClienteTest con métodos como ser testDeberiaFallarCuandoFaltaApellido y testDeberiaFallarCuandoFaltaNombre.

Luego intentaría calcular la edad y entraría en un mundo de reglas de negocio específicas tales como: ¿Qué pasa cuando la edad y fecha de nacimiento que son provistas pero no coinciden? ¿Qué pasa si el cumpleaños es hoy? ¿Cómo calculo la edad si solo tengo la fecha de nacimiento? En este caso me encontraría escribiendo nombre de métodos relativos a edad y fecha de nacimiento y eso indicaría que estos pertenecen a otro objeto con un comportamiento distinto. Esto me indica que debo introducir una clase CalculadorDeEdad, e incluir una prueba llamada CalculatorDeEdadTest. Luego podría ubicar todo comportamiento relativo al cálculo de la edad en este nuevo objeto y de este modo solo necesitaría validar y testear la edad y fecha de nacimiento en un solo lugar.

Si una clase hace más de una cosa, es un aviso que la clase debería ser dividida en dos o más clases. Personalmente, yo definiría el nuevo servicio usando una interface que explique qué hace e inyectaría este servicio en el constructor de la clase:

public class ValidadorDeDetallesDeClient {
   private final CalculadorDeEdad calculadorDeEdad;
   public ValidadorDeDetallesDeClient(CalculadorDeEdad calculadorDeEdad) {
      this.calculadorDeEdad = calculadorDeEdad;
   }
}

Este estilo de enhebrar los métodos se llama Inyección de Dependencias y es muy útil cuando es usado en conjunto con objetos MOCKS.

Una prueba unitaria con un buen nombre brilla cuando el test falla

Programando, me ha pasado de cambiar código que hace que las pruebas unitarias fallen y con solo mirar el nombre del test me puedo dar cuenta cual es la intención de ese código. Cuando esto pasa es por una de las siguientes tres cosas:

  • Porque mi cambio introduce un error. Mi culpa. La solución es arreglar lo que rompí
  • La intención original del código está ahora ubicada en otra parte. La solución en este caso es mover el test también o tal vez cambiar el test
  • La intención original del código ahora no es acertada porque las premisas del sistema han cambiado. La solución en este caso es eliminar el test

Este último caso, es más común en proyectos ágiles, donde el conocimiento del negocio crece con el paso del tiempo. Lamentablemente, los programadores inexpertos en TDD temen borrar pruebas unitarias con las que se encuentran, como si dejando esos test aumentara la calidad de su código.

Es por esto que la palabra “debería” (deberíaRetornarEsto, de should en inglés) es la adecuada para formar parte del nombre de un método en lugar de “tiene que” (tieneQueRetornarEsto) o “va a” (vaARtetornarAquello). “Debería” nos permite desafiar la premisa, con preguntas tales como “¿realmente debería retornar esto o aquello?”. Esto nos ayuda a identificar si una prueba unitaria falla por un error que estamos introduciendo o si simplemente las presunciones que usamos son ahora incorrectas.

“Comportamiento” es una palabra que se expresa mejor que “Prueba”

Ahora que dispongo de una herramienta -agiledox- que me ayuda a eliminar la palabra “test” y que dispongo de una “sentencia genérica” para denominar a cada método, me he dado cuenta que lo que confunde a muchos desarrolladores es la palabra “prueba” o “test”.

Además recordemos que usar pruebas unitarias no significa que se esté haciendo TDD. Esto sería una buena forma de asegurarse que el código funciona. Pero si los métodos no expresan claramente el comportamiento del sistema entonces estos nos llevaran a tener la falsa ilusión de seguridad.

Desde que uso la palabra “comportamiento” (behaviour en inglés) como parte del nombre de los métodos, en lugar de usar “prueba” (test en inglés) cuando hago TDD, me he encontrado que no sólo funciona para mí, sino que ha hecho desaparecer mágicamente muchas preguntas relacionadas. Con esto tengo respuesta a muchas otras preguntas con respecto a TDD.

¿Cuál debería ser el nombre de una prueba? Fácil, el nombre es una frase que describe el comportamiento de esa parte del sistema.

¿Qué tan grande debería ser una prueba? Tan grande como una frase puede describir el comportamiento de una parte del sistema y cuando una prueba falla, simplemente hay que repetir los tres pasos descriptos antes. O he introducido un error yo mismo, o el comportamiento se ha desplazado a otra parte o esta prueba ya no es relevante.

Mi forma de ver TDD se ha desplazado de pensar en pruebas a pensar en comportamientos y es por eso que ahora cuando pienso en TDD lo veo como BDD (Behaviour Driven Development, inglés para desarrollo dirigido por comportamientos).

JBehave enfatiza en comportamiento antes que en pruebas unitarias

Para fines de 2003, decidí invertir mi dinero –o al menos invertir mi tiempo– donde invertía mis palabras. Es por eso que empecé a escribir el reemplazo de JUnit y lo llamé JBehave, que reemplaza cualquier referencia a pruebas con referencias a comportamientos y construyen un vocabulario dirigido a verificar ese comportamiento.

Hice esto para ver hasta dónde podía evolucionar el concepto si yo me adhería por completo a mi idea de “desarrollo dirigido por comportamientos”. Y también quería ver si me serviría para enseñar TDD y BDD sin las distracciones que suponen un vocabulario basado en pruebas unitarias.

Para definir el comportamiento de una hipotética clase “BusquedaDeCliente” (CustomerLookup en inglés) usaría una clase comportamiento llamada “ComportamientoDeLaBusquedaDeCliente” (CustomerLookupBehaviour en inglés). Esta clase contendría métodos con nombres que empiecen con la palabra “debería” (should en inglés). Luego, el programa que ejecuta esos comportamientos, instanciaría cada una de esas clases comportamiento y ejecutaría los métodos de a uno en orden del mismo modo que JUnit lo hace con las pruebas unitarias. Este programa reportaría su progreso y al final imprimiría un resumen con los resultados.

Mi primer paso fue hacer que JBehave pueda verificarse a sí mismo. Sólo agregué comportamientos de modo tal que JBehave pueda ejecutarse a sí mismo. Con eso sólo pude migrar todos mis pruebas unitarias de JUnit a JBehave y obtener el mismo resultado.

¿Cómo determinar cuál es el más importante de los siguientes comportamientos?

Luego descubrí el concepto de “valor agregado al negocio”. Obvio que siempre supe que los programas que escribía tenían una razón de ser, pero nunca pensé el valor que agregaban a mis clientes en el momento que los escribía. Fue cuando otro de mis colegas, Chris Matts, me hizo pensar en el concepto “valor agregado al negocio” en el contexto de “desarrollo dirigido por comportamientos”.

Dado que mi idea era hacer JBehave en una sola pieza o “auto-contenido”, usé una pregunta para mantenerme concentrado. La pregunta es “¿Cuál es la función más importante que el sistema NO hace?”

Esta pregunta me forzó a identificar el valor de las funciones que aún no había implementado y ponerlas en orden de prioridad. También me ayudó a denominar los métodos usando el comportamiento como parte de ese nombre. Por ejemplo en “el sistema no hace X” (donde X es el comportamiento que el sistema todavía no tiene) y dado que X es importante y el sistema debería hacerlo. Por lo tanto el nombre termina siendo:

public void deberiaHacerX() {
    // ...
}

Usando este método, contesto otra pregunta común en TDD, ¿Qué es lo siguiente que debo hacer?

Requerimientos son comportamientos

Después de todo esto, me encontré en un punto que me ayudó a entender –y explicar- como TDD funciona y encontré un camino que evita chocar contra los problemas que yo he chocado en el pasado.

Hacia fines de 2004, mientras le describía a Matt mis descubrimientos, él me dijo “pero eso es análisis”. Permanecimos callados por un momento analizando esa frase y la situación y luego decidimos aplicar esta idea de “comportamientos” al proceso de toma de requerimientos. Sabíamos que si podíamos crear un vocabulario consistente que se pueda usar para el análisis, desarrollo, testeo y para el uso por parte de los integrantes del negocio, entonces podríamos terminar con la ambigüedad y malos entendidos que existen cuando personas técnicas intentan comunicarse con personas no técnicas.

BDD provee un “lenguaje ubicuo” que puede ser usado en análisis

Para la misma época, Eric Evans publica su famoso libro “Diseño dirigido por el dominio” (DDD Domain Driven Design). En ese libro, Eric describe el concepto de “lenguaje ubicuo” que se utiliza para modelar conocimiento y que se basa en el dominio del negocio a analizar de modo que ese lenguaje de dominio se infiltra en el código que los desarrolladores escriben.

En ese momento, con Chris nos dimos cuenta que estábamos tratando de definir un “lenguaje ubicuo” para el proceso de análisis y toma de requerimientos. Habíamos encontrado un buen punto de partida. En nuestra empresa ya existía un patrón para definir historias siguiendo este formato

Como [X]
Yo quiero [Y]
De modo que [Z]

Donde Y es una funcionalidad, Z es el beneficio o valor agregado que se obtiene con esa funcionalidad y X es la persona (o rol) a quién se beneficia. Este formato demuestra un gran beneficio dado que fuerza a quién escribe la historia a pensar el valor que está agregando al sistema cuando escribe esa historia. Cuando no existe real valor agregado, la historia termina siendo algo similar a “como un [usuario], yo quiero [esta función], de modo que [yo obtenga eso que quiero]”. Esto ayuda mucho a remover historias que contienen requerimientos esotéricos.

Para ese momento, habíamos empezado a descubrir lo que toda persona de “QA” trabajando en entornos ágiles sabe, el comportamiento en una historia es también el criterio de aceptación de la misma. Si el sistema cumple con todos los criterios de aceptación entonces el comportamiento será el esperado y si no, el comportamiento será erróneo. De este modo creamos una plantilla para escribir criterios de aceptación.

Esta plantilla tenía que ser lo suficientemente libre como para que no se sintiese artificial y estructurada al mismo tiempo para que nos permita dividir las historias para poder automatizar las pruebas. Es así que empezamos a describir los criterios de aceptación como escenarios siguiendo el siguiente formato

Dado que [se cumple tal o cual condición] (los “givens” en inglés)
Cuando [ocurre tal o cual evento] (los “events” en inglés)
Entonces [nos aseguramos que pasa algo que esperamos] (los “outcomes” en inglés)

En inglés (Given, When, Then)

Para demostrar todo esto, usaré el clásico ejemplo de un cajero automático. Una de las historias podría decir algo como

+Título: Cliente retira dinero+
Como cliente,
yo quiero retirar dinero del Cajero Automático,
de modo que puedo evitar ir al banco a hacer una cola para obtener dinero

La pregunta que surge es ¿cómo se cuándo la historia está cumplida? Pues, hay varios escenarios por considerar: la cuenta tiene que tener dinero o crédito, la cuenta puede estar autorizada a retirar solamente un determinado monto de dinero, la cuenta puede estar excedida, etc. Por supuesto, habrá muchos otros escenarios a considerar.

Usando la plantilla dado-cuando-entonces, los dos primeros escenarios serían algo así:

+Escenario 1: Cuenta tiene crédito+
Dado que la cuenta tiene crédito
y que la tarjeta es válida
y que el cajero tiene dinero disponible
Cuando el cliente pide dinero
Entonces la cuenta es debitada
y el dinero es entregado al cliente
y el cliente recupera su tarjeta

Note el uso de “y” como conector entre varias condiciones y resultados.

+Escenario 2: La cuenta excede el límite negativo acordado con el banco+
Dado que la cuenta excede el límite negativo acordado con el banco
y que la tarjeta es válida
Cuando el cliente pide dinero
Entonces el cajero muestra un mensaje negando el pedido
y el dinero no es entregado al cliente
y el cliente recupera su tarjeta

Ambos escenarios están basados en el mismo evento e incluso tiene algunas condiciones y resultados en común. Era nuestra intención llegar a este punto para poder re-utilizar algunas cosas.

Los criterios de aceptación deberían poder ejecutarse

Los distintos fragmentos de un escenario –los “givens”, los “events” y los “outcomes”– deberían ser tan pequeños como para poder representarlos directamente en el código. JBehave define un modelo de objetos que nos permite referenciar estos fragmentos a clases del lenguaje Java.

La idea es escribir una clase por cada “dado que” (por cada “given”)

public class CuentaTieneCredito implements Given {
	public void setup(World world) {
		...
	}
}
public class TarjetaEsValida implements Given {
	public void setup(World world) {
		...
	}
}

Y luego una clase por evento (por cada “event”):

public class ClientePideEfectivo implements Event {
	public void occurIn(World world) {
		...
	}
}

Y deberíamos hacer algo similar para los resultados (por cada “outcome”). Luego será JBehave el encargado de conectar estos distintos componentes y ejecutarlos en la secuencia adecuada. Lo que creará será un “mundo” que será un contenedor para almacenar otros objetos que serán pasados como parámetros a los distintos métodos “dado que” (Givens) para que estos puedan actualizar sus estados. JBehave luego pasa a los eventos “occur in” que estados tienen esos comportamientos en ese escenario. Finalmente JBehave le pasará el control a los “resultados” que hayamos definido en esta historia.

Teniendo una clase por cada pequeño fragmento nos permite reusar esos fragmentos en otros escenarios e historias. En un principio, esos fragmentos serán implementados usando “dobles” (mocks por ejemplo) para crear una cuenta con crédito disponible o una tarjeta con un número válido. Así daremos los primeros pasos para implementar los distintos comportamientos. A medida que vayamos implementando la aplicación, esos “dado que” y “entonces” (givens y outcomes) serán reemplazados por el uso de clases reales implementados luego y con el paso del tiempo cada escenario será finalizado y el conjunto de todos estos escenarios es lo que llamaremos un “test funcional de punta a punta”.

Presente y futuro de BDD

Luego de un pequeño lapsus, el desarrollo de JBehave es activo nuevamente. El centro del mismo (core) está prácticamente finalizado. Los siguientes pasos son la integración con los editores de Java como ser IntelliJ y Eclipse.

Dave Astels ha estado activamente promocionando BDD. En su blog y a través de varias publicaciones han provocado un remolino de actividad, por ejemplo con el proyecto rspec que es una implementación de BDD para Ruby. Yo también he estado trabajando en rbehave que es una implementación de JBehave para Ruby.

Varios de mis colegas están usando técnicas de BDD en una gran variedad de proyectos y han encontrado grandes beneficios en estas técnicas. El desarrollo de programa que ejecuta las historias de JBehave –la parte que verifica los criterios de aceptación- también está activo.

La visión es tener un editor que pueda ser usado por Analistas de Sistemas y Testers para que ellos puedan capturar los requerimientos de las historias en un editor de textos y desde ahí generar el esqueleto para las clases comportamiento, todo esto sin salir de su lenguaje de negocios. BDD evoluciona con la ayuda de muchas personas y yo me siento muy alagado por eso.

La versión original está en el Blog de Dan North

From → IT

One Comment
  1. Anonymus permalink

    Muy buena información, gracias por compartirla!

Leave a comment