BDD

BDD (Behavior Driven Development) o Desarrollo Dirigido por Comportamiento es una metodología que lleva varios años en el tintero, pero día a día va cobrando más fuerza. En este artículo aprenderemos más sobre ella.

Historia de BDD

BDD partió de distintas metodologías ágiles, que primero fueron comentadas sobre los años 2000 por Dan North. Posteriormente, Dan publicó un artículo llamado Introducing BDD, donde comenzó a desarrollar más el tema. Finalmente, el concepto de BDD terminó haciéndose un estándar, con la ayuda de Elizabeth Keogh en las charlas que dieron durante la “Agile specifications, BDD and Testing eXchange” celebrada en el año 2009.

Y es que BDD nació como respuesta a los problemas que surgían al empezar con TDD (Desarrollo Dirigido por Pruebas). Ya que la mayoría de programadores y programadoras se encontraban con las mismas preguntas:

  • ¿Dónde comienza el proceso de tests?
  • ¿Qué probar y qué no probar?
  • ¿Cuánto abarca una prueba?
  • ¿Cómo llamar a las pruebas?
  • ¿Cómo entender por qué falla una prueba?

Conceptos de BDD

Inicialmente, la idea de nuestros amigos Dan y Liz era que los tests más complejos se definieran de forma similar a las historias de usuario. Por lo que cualquier persona sin conocimientos técnicos podría plantear las pruebas. Es por ello, que partiendo de la típica definición de historia en el desarrollo ágil:

Como [rol que ejecuta la prueba]
Quiero [pasos a realizar]
Por lo que [resultado esperado]

Se pasó a una definición de comportamientos y escenarios. Así que ya no definiremos una prueba como tal, sino que pensaremos en el comportamiento que debería tener dicha aplicación. Es por ello, que al desarrollar con BDD, debemos pensar: "¿cuál es la función más importante que la aplicación no hace?". Con esa idea en mente, podremos pasar de pensar "no sé que probar" a tener un comportamiento con "la aplicación debería hacer X".

Una vez que pensamos en comportamientos, en BDD se plantea el siguiente esquema:

Comportamiento: qué se espera que haga el sistema
  Escenario: caso que define el comportamiento
    Requisitos previos
    Operaciones para lograrlo
    Resultado esperado

Esta definición comprende las siguientes partes:

  • Comportamiento a probar, o funcionalidad que se espera que cumpla el sistema. Por ejemplo, si estamos en una tienda online, el funcionamiento del carrito de la compra. Un comportamiento se compone de distintos escenarios.
  • Los escenarios son los casos que ayudan a verificar el comportamiento. Siguiendo con el ejemplo anterior, pueden ser escenarios: añadir un artículo, eliminar un artículo, cambiar la cantidad, etc. Un escenario se compone de lo que se llama pasos, éstos se encuentran agrupados en:
    • Requisitos, es aquel estado inicial que deja el escenario listo para probar.
    • Las operaciones son los pasos a seguir para probar el escenario.
    • El resultado, tal y como su nombre indica, es lo que se desea verificar tras seguir los pasos.

El conjunto de pasos de cada escenario, se pueden relacionar con el sistema de tests en cuatro fases de la forma siguiente:

BDD and Four Phase Testing

Se puede observar como la fase "Tear down", no existe de base en BDD, lo que no quita que frameworks como Cucumber permitan tener esta fase en BDD.

Ventajas de BDD

Tras haber visto un poco la introducción a BDD, lo siguiente que podéis pensar es ¿qué ventajas puede acarear el definir así las pruebas?

Define un estándar para que negocio y desarrollo puedan entenderse
Para ello, se utiliza un lenguaje natural, o muy aproximado al lenguaje natural, de forma que cualquiera puede aprenderlo sin tener conocimientos técnicos.

Ayuda a comprender mejor qué se espera del desarrollo de la historia
Gracias a la naturalización de los conceptos y las definiciones (sin tener abreviaturas y huyendo de los términos técnicos de cada tecnología), se puede tener un objetivo claro en mente que ayude a comprender que se debe llevar a cabo.

Cualquier persona relacionada con la aplicación puede definir los casos de uso
Tal y como comentamos antes, al usarse un lenguaje sencillo, no importa que la persona que quiera definir los comportamientos no tenga conocimientos técnicos. Eso sí, será importante que sí tenga conocimiento de negocio para evitar casos innecesarios o poco definidos. Ya lo decía Martin Fowler en su charla Not Just Code Monkeys:

"No es tan importante saber determinados lenguajes, estos van y vienen, y en realidad no son tan diferentes entre sí. Aprender el dominio en el que estás trabajando. Esa sí que es una habilidad muy útil."

Martin Fowler

En independiente del framework de pruebas utilizado
BDD nos da una serie de pautas para definir los comportamientos, es por ello que, al igual que los patrones de diseño se pueden aplicar a prácticamente cualquier lenguaje, todos los frameworks que usemos para BDD serán muy similares.

Es muy fácil convertir la definición de una historia en casos BDD y viceversa
Ya hemos visto que los pasos que definen un escenario partían de la definición estándar de historias en el desarrollo ágil. Es por ello, que la transcripción de una historia a BDD es algo casi automático, si hemos definido la historia de forma correcta.

BDD + TDD

Recapitulando, hemos visto hasta ahora que, con BDD tenemos una forma estándar de poder definir casos de prueba, así como de llevarlos a cabo. Pero ¿cómo encaja esto en un flujo de desarrollo habitual? Pues bien, BDD, se basa en los mismos principios que TDD, es decir, se escribe un caso que falle, se desarrolla y/o se refactoriza hasta hacer que funcione.

BDD y TDD
Flujos BDD y TDD colaborando

Como podéis ver en la imagen anterior:

  • Escribimos un escenario que falle.
  • Comenzamos con TDD hasta que todos los tests pasen.
  • Una vez que pasen todos los test de lo que podríamos decir, que es la parte TDD, volvemos al escenario para ver si el escenario se prueba correctamente. Si no es así, seguiremos refactorizando.

BDD en la pirámide de tests

No hemos mandado a BDD de viaje por Egipto como si fuera una historia de Astérix, sino que vamos a ver como se integraría BDD en la famosa pirámide introducida por Mike Cohn en su libro Succeeding with Agile.

Pirámide de tests

Para aquellas personas que no conozcáis la pirámide de test, básicamente nos indica que peso en la aplicación debe tener cada tipo de test. De forma que, de forma resumida, debe haber más tests unitarios y menos tests manuales. La pirámide de la imagen tiene algunos cambios respecto a la original, basado en mi experiencia, así que la podríamos llamar la pirámide V2.

Paso a comentaros que es cada escalón:

  • Tests unitarios: permiten probar pequeños componentes o funcionalidades sencillas para verificar que funcionan como se espera de forma aislada.
  • Tests de componentes: siguen siendo tests unitarios pero que agrupan varias funcionalidades en una. Por ejemplo, un test unitario puede aplicarse a un elemento de un menú, y un test de componente lo podríamos ejecutar sobre todo un menú compuesto de distintos elementos. En el caso de un servicio REST, un test unitario podría ser la creación de un usuario, y un test de componente, toda la API para gestión de usuarios.
  • Tests de integración: hasta ahora, los dos tipos de tests anteriores si requerían de alguna entidad externa, se creaban mocks para simularla. Sin embargo, con este tipo de tests lo que hacemos es verificar que el comportamiento de nuestro sistema es el esperado cuando interacciona con elementos externos. Por ejemplo, tenemos un servicio REST que llama a otro servicio. Este tipo de tests pueden requerir bastante configuración, es por ello que aunque importantes, no tienen tanto peso como los dos escalones anteriores.
  • Tests de sistema o de aceptación: se aplicaría o a la interfaz de usuario, pero también implica pruebas en sistemas sin interfaz gráfica. En este escalón lo que queremos es probar que todo el conjunto de nuestro sistema opera correctamente. Son los tests automáticos que tardan más en ejecutarse, es por ello que normalmente habrá menos tests de sistema.
  • Test manuales: toda prueba que realiza una persona. Este tipo de tests dependen mucho de quién los realice, es por ello que puede darse el caso que no sean todo lo fiable que nos gustaría.

Esta pirámide no es algo obligatorio a seguir, incluso dependiendo del tipo de aplicación, puede que se llegue a dar incluso una pirámide invertida, donde los tests de sistema tengan más peso que los unitarios, una en formato trofeo, donde los de integración sean los importantes, etc. Como todo en el mundo de la programación, no viene prefijado y se puede ajustar según necesidad.

Tras el inciso para hablar de la pirámide, podéis ver que BDD se integraría en los tests de componentes, integración y sistema. No tendrían sentido a nivel de tests unitarios, puesto que tendríamos que definir con BDD hasta el más mínimo comportamiento del método más pequeño, esto sería algo muy engorroso.

Por otro lado, respecto a los tests manuales, a una persona se le puede dejar los comportamientos de BDD para que los pruebe, pero es mejor evitar guiar a quien testea, puesto que siempre tenderá a ceñirse a los escenarios definidos en BDD. Sobre como plantear los test manuales podemos profundizar más en otro artículo, sí así gustáis.

Gherkin. Un lenguaje para dominarlos a todos

Una vez que sabemos como proceder, nos falta conocer el estándar. A nivel conceptual con BDD, no es obligatorio casarse con ningún lenguaje, pero prácticamente el que ha prevalecido sobre los demás, es Gherkin. Éste permite definir las pruebas de una forma natural y expresiva. Pero como todo lenguaje tiene ciertas palabras clave. Y estas son:

Feature: comportamiento a probar.
  Scenario: especifica un casos de uso del comportamiento.
  Given condiciones previas al escenario.
  When acciones a ejecutar en el escenario.
  Then resultados esperados.

Ya habéis podido comprobar como esa estructura se parece mucho a lo que vimos al principio, y que como la definición de una historia de usuario puede fácilmente adaptarse a ese esquema.

De ahora en adelante, los ejemplos tendrán las definiciones en inglés, simplemente para que podáis ver algo más real, aunque hay librerías como Cucumber que permiten usar otros idiomas, como por ejemplo, el español. De todas formas, en la mayoría de los casos seguro que os encontráis con definiciones en inglés.

Comencemos con un ejemplo de un caso de prueba en la especificación de un supuesto reproductor de vídeo podría ser:

Feature: Video Player
  Scenario: pause a video
    Given a playing video
    When user pauses the video
    Then the video becomes paused

Esta claro que la especificación anterior podría ser escrita por cualquier persona sin conocimientos técnicos. Y es por ello que Gherkin es tan potente.

Pero claro, seguro que echas en falta poder indicar valores a las pruebas, pues bien, en Gherkin se pueden indicar valores para que sean tenidos en cuenta en las pruebas. Sigamos con nuestro reproductor de vídeo y pensemos en un botón que salte un segundo hacia delante. Un escenario podría ser:

Scenario: move one second forward
    Given a video that is paused in timecode "00:00:00"
    When user advanced the video one second
    Then the video remains paused and timecode is "00:00:01"

Ese valor entrecomillado es un atributo que nuestras pruebas podrán leer, aunque por ejemplo, con los valores numéricos no es necesario.

Eso sí, podemos ir más allá, y trabajar incluso con tablas. Lo siguiente ejecutará una prueba por cada fila de la tabla:

Scenario Outline: multiply two numbers
  Given the <multiplier> multiplier
  When the multiplying is <multiplying>
  Then then the result is <result>

Examples:
  | multiplying | multiplier | result |
  | 12          | 4          | 48     |
  | 137         | 23         | 2921   |
  | 34          | 89         | 3026   |

En este caso, se puede apreciar como es posible recoger los valores de cada columna encerrando el nombre de ésta entre los símbolos < y >.

Pero esto no termina aquí. Gherkin tiene muchos más operadores y palabras clave:

  • And: permite añadir una condición a cualquiera de los pasos
  • But: condición negativa para cualquier paso
  • Background: define una condición que se cumple antes de todos los escenarios.
  • Scenario Outline: permite repetir un escenario varias veces con un conjunto de datos.
  • |: se usa para delimitar tablas.
  • @: define etiquetas para filtrar escenarios.
  • #: al inicio de una línea indica comentario.
  • “””: permite definir un texto de varias líneas.

Y con todo lo anterior podemos tener casos todo lo complejos que queramos, como por ejemplo, el siguiente, en el que vamos a simular un almacenamiento en la nube:

@nocturnal
Feature: Online Files Management
  This feature specify how to opérate with files in our cloud drive.

  # Con el outline definimos pasos a realizar antes de los test, es como
  # por ejemplo, beforeEach en jest o, SetUp en NUnit.
  Background: login as user
    Given the login page
    When username is "pepito"
    And password is "abcd"
    And user logins in

  Scenario: download file from existing asset
    Given a filepath "/autores/Cervantes"
    When open options of file "El Quijote.pdf"
    And download the file
    Then file starts to download

Qué evitar hacer en Gherkin

Para mejorar nuestras definiciones de comportamientos hay dos cosillas que deberíamos evitar siempre:

Usa lenguaje de negocio no tecnológico

Hay que evitar definir los pasos de los escenarios de forma dependiente a la tecnología, es decir, deberiamos evitar frases como "haz click", "realiza una llamada POST", etc. Por un lado, porque podremos reutilizar los escenarios entre distintas partes del proyecto; y por otro, y más importante, porque quién defina la historia, ya dijimos que puede que no tenga conocimientos técnicos, de forma que no tiene por qué saber que por ejemplo una llamada va por API REST, por WS, etc.

  Scenario: create a user
    Given the hability to create users
    When user is "newUser"
    And password is "newPassword"
    And create the user
    Then the user is created

Con el ejemplo anterior podemos ver como el segundo escenario es posible utilizarlo en una parte back y front. En la primera se tomarán credenciales de un rol que pueda crear usuarios y el test terminará realizando una llamada a un API, y en la segunda se realizarán todos los pasos necesarios para crear el usuario desde interfaz.

Evitar pasos que puedan tener doble sentido

Es importante evitar utilizar nombres de steps que tengan distintos contextos. Si por ejemplo, tenemos un paso con el contenido "se borra el texto", seguramente dependerá del escenario en qué se use. Es por ello, que es preferible tenemos pasos con contenidos como "se borra el nombre de usuario", "se borra el título del artículo", etc. Por un lado, queda mucho más claro qué hace el paso, y por otro permite evitar confusiones.

Antes de terminar

Una vez que hemos aprendido algo sobre BDD y Gherkin es manos de ponerse a la obra, así que ¿cómo podemos seguir? Pues, para aplicar esto tenéis frameworks como Cucumber que nos ayuda a implementar BDD. Pero, recordad que BDD es una metodología, por lo que no os obliga a casaros con ningún framework y lo podéis implementar libremente.

Comparte este artículo con quien quieras
Plantilla de proyecto TypeScript

Leave a Comment

Your email address will not be published. Required fields are marked *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.