Estamos acostumbrados a diseñar nuestros tests desde un punto de vista puramente técnico, pero esta no es la única manera posible. Utilizando un lenguaje más natural y usando un punto de vista más de negocio nos permitirá involucrar a más gente en el desarrollo de los mismos (project managers, support, etc.).
Hoy quiero mostraros como hacer BDD en Python y ver que ventajas nos aporta.
¿Qué es BDD?
BDD, cuyas siglas significan Behavior-Driven Development, es una técnica de diseño y desarrollo derivada de TDD (Test-Driven Development) lo que tiene ciertas implicaciones como:
- Los tests se realizaran antes de empezar con el código.
- Haremos iteraciones cortas dentro de un ciclo
Red - Green - Refactor
.
Centrándonos en BDD sus principales diferencias y ventajas frente a otros tipos de testing son:
- Los tests se escriben como casos de uso y como tal sirven como documentación.
- Los casos de uso nos hablarán del comportamiento no de la implementación.
- Estos casos de uso se escribirán usando un lenguaje natural y común a toda la organización (Soporte, Project Managers, Stakeholders, etc.).
- Los casos de uso se crean en ficheros con extensión
.feature
. - Existen palabras clave como
Feature
,Scenario
,Given
,When
oThen
. - Los pasos de los casos de uso se conocen como
steps
y en ellos introduciremos la lógica de nuestros tests.
Conceptos generales de BDD (Given, When & Then)
Para definir los casos de uso en BDD se debe usa el patrón Given-When-Then
, que se define de la siguiente forma:
- Given: En este paso definiremos el estado inicial de nuestro caso de uso.
- When: En este paso realizaremos las acciones o eventos (se recomienda tener un solo
When
por escenario). - Then: En este paso describiremos el estado final esperado de nuestro caso de uso.
Así mismo haremos uso de términos como:
- Feature: Para agrupar nuestros casos de uso.
- Scenario: Para crear un caso de uso (por cada escenario haremos uso de
Given-When-Then
). - Scenario Outline: Para ejecutar un mismo caso de uso con diferentes valores de entrada.
- Examples: Definición de variables de entrada a los tests y sus valores.
Un ejemplo práctico de caso de uso sería:
Feature: Usuarios
Scenario: Listado de usuarios activos
Given: Cuando consultamos la lista de usuarios activos
When: Cuando se carga la lista.
Then: Debemos obtener una lista de usuarios
BDD en Python
BDD está disponible en prácticamente todos los lenguajes de programación y Python no iba a ser menos.
En este post voy a usar una librería llamada Behave, ya que tiene una muy buena comunidad, es muy sencilla de usar y está bastante actualizada.
A raíz de publicar el post me comentaron que Behave no está tan actualizada como yo creía y que a día de hoy es más recomendable usar el plugin BDD de pytest, os dejo aquí el enlace por si lo queréis mirar.
En su propia documentación nos detallan su configuración mínima para que funcione:
features/MY_FEATURE.feature
features/steps/MY_STEPS.py
Es decir necesitamos al menos:
- Una carpeta
features
.- Esta carpeta debe contener:
- Un fichero con extension
.feature
para nuestros casos de uso. - Una carpeta
steps
.- Esta carpeta debe contener:
- Un fichero Python que contendrá nuestros steps.
- Esta carpeta debe contener:
- Un fichero con extension
- Esta carpeta debe contener:
Resolvamos la kata FizzBuzz usando BDD
Una vez hablado de que es BDD y como usarlo en Python vamos a resolver una kata muy sencilla llamada FizzBuzz y que nos va a permitir centrar nuestras energías en los tests con BDD en lugar de en su lógica.
Creamos un proyecto nuevo con Poetry
Creamos una carpeta llamada fizzbuzz
y dentro de ella ejecutamos poetry init -n
(si no tienes instalado Poetry lo puedes instalar con pip install poetry
).
A continuación añadimos los paquetes behave
y expects
a nuestro proyecto ejecutando poetry add behave expects
.
En este punto ya podríamos ejecutar casos de uso con el comando poetry run behave
.
Creamos nuestro primer caso de uso
Creamos un fichero .feature
dentro de una carpeta features
con el siguiente
contenido:
Feature: FizzBuzz
Scenario: FizzBuzz for number 3
Given the number 3
When calculates the result
Then the result should be Fizz
Si volvemos a ejecutar el comando poetry run behave
veremos que nuestro caso
de uso ya está disponible.
Behave tiene una cosa superinteresante y es que si detecta que faltan los steps del caso de uso nos los sugiere cuando lanzamos el caso de uso:
You can implement step definitions for undefined steps with these snippets:
@given('the number 3')
def step_impl(context):
raise NotImplementedError('STEP: Given the number 3')
@when('calculates the result')
def step_impl(context):
raise NotImplementedError('STEP: When calculates the result')
@then('the result should be Fizz')
def step_impl(context):
raise NotImplementedError('STEP: Then the result should be Fizz')
Aprovechemos estos snippets para completar nuestros pasos en el siguiente apartado.
Creamos los steps necesarios para nuestro caso de uso
Como hablábamos al principio del post para hacer el mapeo entre los keywords Given-When-Then
y nuestro steps debemos hacer uso
de los decoradores de behave con el mismo nombre (@given, @when, @then
) en
nuestros métodos .
Estos métodos reciben como parámetro un objeto context
que se puede ver como una variable
global que se va pasando entre steps y donde podremos almacenar valores para
usar en steps posteriores.
Dicho lo cual creamos un fichero steps.py
dentro de una carpeta features/steps
con el siguiente contenido.
from behave import given, when, then
from expects import equal, expect
from fizzbuzz.main import FizzBuzz
@given('the number 3')
def number(context):
context.number = 3
@when("calculates the result")
def calculates(context):
context.result = FizzBuzz.calculates(num=context.number)
@then('the result should be Fizz')
def validates(context, result):
expect(context.result).to(equal("Fizz"))
Primera implementación de FizzzBuzz
Una vez creados los test y ver que estamos en la fase en Red
del ciclo BDD, procedemos a crear un fichero main.py
donde ira el algoritmo para resolver la kata.
Como primer babe step vamos a hacer mínimo código necesario para pasar nuestro test.
class FizzBuzz:
@staticmethod
def calculates(num):
return "Fizz"
Siguiente iteración FizzBuzz
Ahora mismo nuestra lógica funciona perfectamente pero sabemos que esta lejos de estar completa, necesitamos añadir nuevos tests que nos obliguen a modificar el código de producción.
Para ello vamos a editar tanto el .feature
como el steps.py
para añadir un nuevo caso de uso:
Feature: FizzBuzz
Scenario: FizzBuzz for number 3
Given the number 3
When calculates the result
Then the result should be Fizz
Scenario: FizzBuzz for number 5
Given the number 5
When calculates the result
Then the result should be Buzz
from behave import given, when, then
from expects import equal, expect
from fizzbuzz.main import FizzBuzz
@given('the number 3')
def number_3(context):
context.number = 3
@given('the number 5')
def number_5(context):
context.number = 5
@when("calculates the result")
def calculates(context):
context.result = FizzBuzz.calculates(num=context.number)
@then('the result should be Fizz')
def validates_3(context):
expect(context.result).to(equal("Buzz"))
@then('the result should be Buzz')
def validates_5(context):
expect(context.result).to(equal("Buzz"))
Si ejecutamos poetry run behave
veremos uno de los casos de uso en rojo. Actualicemos nuestro algoritmo para volver a la fase Green
:
class FizzBuzz:
@staticmethod
def calculates(num):
return "Fizz" if num % 3 == 0 else "Buzz"
Si ejecutamos nuevamente poetry run behave
veremos los dos casos de uso en verde.
Fase Refactor
Dentro del ciclo BDD la fase de refactor es muy importante y debemos dedicarle tiempo. En nuestro caso ya podemos ver que hay ciertas duplicaciones que se pueden mejorar, asi que editemos tanto el .feature
y el steps.py
:
Feature: FizzBuzz
Scenario: FizzBuzz for number 3
Given the number "3"
When calculates the result
Then the result should be "Fizz"
Scenario: FizzBuzz for number 5
Given the number "5"
When calculates the result
Then the result should be "Buzz"
from behave import given, when, then
from expects import equal, expect
from fizzbuzzd.main import FizzBuzz
@given('the number "{number}"')
def number(context, number):
context.number = number
@when("calculates the result")
def calculates(context):
context.result = FizzBuzz.calculates(num=context.number)
@then('the result should be "{result}"')
def validates(context, result):
expect(context.result).to(equal(result))
Con este cambio estamos haciendo uso de que en behave se conoce como step parameters
, que no es ni mas ni menos que la posibilidad de reutilizar steps pasandole diferentes valores de entrada al mismo.
Si ejecutamos los tests nuevamente todo debería seguir en verde.
Introducimos ejemplos
Como primera aproximación no está mal, pero podemos ir más allá aun incluyendo el
concepto de Examples
en nuestros casos de uso, editemos el .feature
:
Feature: FizzBuzz
Scenario Outline: FizzBuzz calculations
Given the number "<number>"
When calculates the result
Then the result should be "<result>"
Examples: FizzBuzzs
| number | result |
| 3 | Fizz |
| 5 | Buzz |
| 15 | FizzBuzz |
Hemos añadido un nuevo ejemplo lo que nos obliga a cambiar nuestro código de produccion, nuevamente con el mínimo código posible para que pasan los tests:
class FizzBuzz:
@staticmethod
def calculates(num):
if num % 15 == 0:
return "FizzBuzz"
return "Fizz" if num % 3 == 0 else "Buzz"
Si lanzamos nuevamente poetry run behave
deberíamos volver a tener todos lo tests en
verde.
Solución final
Para acabar vamos a incluir los últimos tests que nos permitan cubrir todos los posibles escenarios de nuestro problema:
Feature: FizzBuzz
Scenario Outline: FizzBuzz calculations
Given the number "<number>"
When calculates the result
Then the result should be "<result>"
Examples: FizzBuzzs
| number | result |
| 2 | 2 |
| 4 | 4 |
| 8 | 8 |
| 3 | Fizz |
| 6 | Fizz |
| 9 | Fizz |
| 5 | Buzz |
| 10 | Buzz |
| 20 | Buzz |
| 15 | FizzBuzz |
| 30 | FizzBuzz |
| 45 | FizzBuzz |
Lo que nos fuerza a actualizar nuestro algoritmo para estos nuevos ejemplos:
class FizzBuzz:
@staticmethod
def calculate(num):
result = ""
if num % 3 == 0:
result += "Fizz"
if num % 5 == 0:
result += "Buzz"
return result if result else f"{num}"
¡Y deberíamos tener de nuevo los tests en verde!
Conclusiones
Y hasta aquí está breve introducción a BDD, espero que os haya gustado y que os pueda servir de cara a introducirla en vuestro flujo de trabajo si creéis que os puede aportar valor. Os dejo por aquí el código en un repositorio por si queréis echarle un ojo.
!Un saludo!