Adrianistán

Testing en lenguaje natural con Gherkin y Behave

20/02/2020

El testing es fundamental para asegurar que nuestras aplicaciones cumplen con unos requisitos de calidad mínimos. Existen muchos tipos de test y formas de hacer tests. En este artículo vamos a ver el Behavior Driven Development y como lo podemos aplicar en Python con Behave.

Behavior Driven Development

El BDD es una técnica que surge a raíz del TDD (Test Driven Development). La esencia del TDD es que tenemos que escribir los tests antes que el propio código. BDD va un paso más allá, tenemos que hacer tests que sirvan para describir las especificaciones en un lenguaje que pueda editar un no-programador. La idea es que pueda haber alguien de producto, que defina las especificaciones y que a su vez son tests de aceptación.

El lenguaje más usado en BDD es Gherkin, cuya base son las sentencias Given, When y Then. Una de las implementaciones de Gherkin en Python más usadas es Behave. Con Given debemos añadir todos los pasos necesarios para llegar hasta el punto donde queremos realizar el test. En When realizamos un estímulo, lo que se prueba. Idealmente sería una única sentencia. Con Then comprobamos que el resultado del estímulo es el esperado. Veamos un ejemplo. Todos ellos admiten combinaciones mediante And y But


Feature: comments

Scenario: add a comment in blog post
Given a post page is loaded
When I add a comment
Then the page is reloaded
And the page shows a confirmation message
And the comment is registered in the database

Scenario: approve comment
Given the comment is registered in the database
And I can't see the comment in the post page
When I approve the comment
Then I can see the comment in the post page

Cada paquete de Give/When/Then se denomina escenario y una feature o característica se compone de varios escenarios. Las features se guardan en un fichero .feature dentro de la carpeta features. Dentro de la carpeta features, si usamos Behave, tendremos una carpeta llamada steps que tendrá dentro código Python para implementar los tests correctamente. Para ello usamos el decorador step. Las funciones admiten un parámetro siempre, llamado context, que es propio de Behave y que puede usarse para mover estado entre steps.


from behave import *

@step("a post page is loaded")
def post_page_load(context):
    pass

@step("I add a comment")
def add_comment(context):
    pass

@step("the page is reloaded")
def page_reloaded(context):
    assert True is True

@step("the page shows a confirmation message")
def page_shows_confirmation_message(context):
    assert True

@step("the comment is registered in the database")
def comment_registered_in_database(context):
    assert not False

@step("I can't see the comment in the post page")
def cant_see_comment(context):
    assert True

@step("I can see the comment in the post page")
def can_see_comment(context):
    assert True

@step("I approve the comment")
def approve_comment(context):
    assert True

En este caso vamos a dejar los tests vacíos, pero aquí habría que implementar una lógica real de test, por ejemplo, con Selenium para interactuar con la web y comprobar que efectivamente la funcionalidad está implementada y funciona tal cual se ha especificado.

Para ejecutar los tests, escribimos behave

Variables y tablas

Estos steps deben describir en lenguaje natural las precondiciones, la interacción y las postcondiciones pero puede que queramos reutilizar steps o parametrizarlos. Para ello tenemos variables y tablas

Las variables se definen en el step rodeadas de llaves y en la función del step aparecen como una variable más en los argumentos de entrada. Por ejemplo podemos modificar el siguiente escenario para añadir un texto de ejemplo.


Scenario: add a comment in blog post
Given a post page is loaded
When I add a comment with text "your post looks insteresting"
Then the page is reloaded
And the page shows a confirmation message
And the comment is registered in the database with text "your post looks interesing"

Y en los steps:


@step("I add a comment with text \"{text}\"")
def add_comment(context, text):
    pass

@step("the comment is registered in the database with text \"{text}\"")
def comment_registered_in_database(context, text):
    assert not False

Sin embargo si nuestro texto es muy grande podemos añadirlo de forma adjunta y accesible a través de la variable context.text


Scenario: add a comment in blog post
Given a post page is loaded
    """
    Lorem Ipsum
    """
When I add a comment 
Then the page is reloaded
And the page shows a confirmation message
And the comment is registered in the database

@step("a post page is loaded")
def post_page_load(context):
    print(context.text)

También existen tablas, que son accesibles a través de la variable context.table


Scenario: add a comment in blog post
Given a post page is loaded
When I add a comment 
    | text | username |
    | Hey! | aarroyoc |
    | Bye! | marlogui |
Then the page is reloaded
And the page shows a confirmation message
And the comment is registered in the database with text "your post looks interesing"

@step("I add a comment")
def add_comment(context):
    for row in context.table:
        print(row["text"])

Environment, context, tags y hooks

Podemos controlar el entorno a través del fichero environment.py que tiene que estar en la carpeta features. En este fichero podemos definir hooks, que son funciones que se ejecutan antes o después de un determinado paso/escenario/feature. Pueden servirnos para realizar tareas de limpieza o de setup.


class WebClient:
    def __init__(self):
        pass
    def stop(self):
        pass

def before_all(context):
    context.client = WebClient()

def after_all(context):
    context.client.stop()

Los hooks disponibles son before_{all, step, scenario, feature} y after_{all, step, scenario, feature}.

Tanto en los hooks como en los steps vemos la variable context siempre. Se trata de una variable controlada por Behave que puede servirnos para pasar información entre los steps y los hooks. Podemos crear por ejemplo, un cliente web con un hook y luego acceder a él en el step gracias a la variable context. O entre distintos steps, pasar información más precisa que no se define en el fichero con Gherkin. Además, context tiene algunas variables predefinidas, como los tags.

Los tags son anotaciones que se ponen sobre los steps, escenarios o features. Si bien en general son texto libre, algunos tienen efecto sobre Behave


Feature: comments

@data-1
Scenario: add a comment in blog post
Given a post page is loaded
When I add a comment 
Then the page is reloaded
And the page shows a confirmation message
And the comment is registered in the database

@wip
Scenario: approve comment
Given the comment is registered in the database
And I can't see the comment in the post page
When I approve the comment
Then I can see the comment in the post page

En el primer caso de tag (data-1), es algo que me he inventado, en un hook podemos comprobar los tags y actuar en consecuencia. En este caso, este tag quiere decir que para este escenario carguemos en la base de datos el conjunto de prueba 1.


def before_scenario(context, scenario):
    if "data-1" in context.tags:
        # Load Data-1
        pass

En el segundo caso, el tag wip es reconocido por Behave y sirve para marcar tests que estamos probando. Estos los podemos ejecutar por separado indicando -w al llamar a Behave.


behave -w

¡Con esto ya tenemos lo suficiente para ir desarrollando tests al estilo BDD! Por supuesto, existen frameworks similares en otros lenguajes, como Cucumber en Java, pero dependiendo de los test que queramos hacer no será necesario usar el mismo lenguaje que la propia aplicación (podemos usarlo para testear desde fuera, por ejemplo, a través una API REST o desde una web).

Tags: programacion testing linux tutorial behave bdd python