Desarrollo guiado por comportamiento (BDD)
8.1 Definición y principios
El desarrollo guiado por comportamiento, del ingĺes BDD (behavior-driven development) es una forma especializada de Lógica de Hoare aplicada a TDD la cual se centra en la especificación del comportamiento del software utilizando unidades del lenguaje de dominio de la situación.
De forma análoga a TDD, BDD se centra en los pasos:
- Definición del test.
- Implementación de la lógica que cumple el test.
- Verificación de la ejecución del test, y el cumplimiento de la especificación.
El desarrollo guiado por comportamiento especifica que tests de unidades de software deben cumplimentar unidades de comportamiento deseado.
Extrayendo de desarrollo ágil el "comportamiento deseado" en este caso consiste en los requerimientos definidos por el negocio, esto es, el comportamiento deseado que tiene valor para el negocio, sea cual sea la entidad de software responsable que esté bajo construcción.
Estos comportamientos deseados deberían estar definidas como historias de usuario en un lenguaje común entre desarrolladores y el resto del equipo involucrado en el proyecto.
Para ello BDD elige utilizar un formato semi-formal para la especificación de comportamiento que está tomado de las especificaciones de la historia de usuario desde el análisis y diseño de la aplicación.
BDD especifica que los analistas de negocio y desarrolladores deben colaborar en esta materia y deben especificar el comportamiento en términos de historias de usuario, que debe quedar reflejada de forma expresa en un documento específico. Cada historia de usuario debe, de alguna manera, seguir la siguiente estructura:
Título La historia debe tener un claro, título explícito.
Narrativa
Una breve descripción introductoria que especifica:
- Quien (que negocio o función del proyecto) es el responsable o los grupos de interés principal de la historia (el actor que se deriva beneficio de la historia).
- El efecto que el actor quiere que la historia tenga.
- El valor que se le asigna a la historia de usuario.
Los criterios de aceptación o escenarios
Una descripción de cada caso específico de la narración. Este escenario tiene la siguiente estructura:
- Se inicia mediante la especificación de la condición inicial que se supone que es cierto en el comienzo del escenario. Esto puede consistir en una sola cláusula, o en varias cláusulas.
- Los eventos que desencadenan el inicio del escenario.
- Por último, se establece el resultado esperado, en una o más cláusulas.
8.2 Behat
Para el lenguaje PHP existe un proyecto de código libre llamado Behat [24] que nos da las herramientas necesarias para poder realizar este tipo de desarrollos guiados por comportamiento. Para poder interpretar las historias de usuarios de forma automática, Behat interpreta el lenguaje Gherkin. Gherkin es un lenguaje específico de dominio que permite describir comportamiento de software sin necesidad de que el comportamiento esté desarrollado.
El ejemplo que vamos a desarrollar en esta sección es, a posteriori, un conjunto de tests para verificar el comportamiento del sistema de blogs más extendido en la comunidad PHP: WordPress. Hemos decidido realizar este ejemplo por dos motivos: simplicidad y funcionalidad. Aunque en nuestro ejemplo no vamos a especificar funcionalidades nuevas, que es el principal objetivo de Behat, también puede ser utilizado como framework para tests de aceptación y ser extendido posteriormente para crear test en BDD. Con esto queremos matizar la posibilidad de crear test de regresión en proyectos con código no testeado para la posterior creación de nuevas funcionalidades siguiendo BDD.
Para nuestro ejemplo, además de Behat necesitamos otro proyecto, también de código abierto, llamado Mink. Este proyecto es un conector entre Behat y los distintos simuladores o conectores con los navegadores web. Dado que lo que deseamos testear en nuestro caso es una proyecto web (un blog basado en Wordpress como test de aceptación), necesitamos de esta herramienta para realizar las distintas interacciones con la el blog. Además de Mink, y como simulador web, utilizaremos Selenium.
Actualmente hay otras alternativas a Selenium con distintas funcionalidades, en función de si son emuladores de navegadores o clientes HTTP. A continuación presentamos la tabla 8.1, en la que detallamos de los diferentes conectores que hay disponibles para la versión actual de Mink (1.7.0).
Funcionalidad | BrowserKit/Goutte | Selenium2 | Zombie | Selenium | Sahi |
---|---|---|---|---|---|
Navegación entre páginas | Sí | Sí | Sí | Sí | Sí |
Manipulación formularios | Sí | Sí | Sí | Sí | Sí |
Autenticación HTTP auth | Sí | No | Sí | No | No |
Manipulación de ventanas | No | Sí | No | Sí | Sí |
Manipulación de iFrames | No | Sí | No | Sí | No |
Acceso a cabeceras de petición | Sí | No | Sí | No | No |
Acceso a cabeceras de respuesta | Sí | No | Sí | No | No |
Manipulación de cookies | Sí | Sí | Sí | Sí | Sí |
Acceso a códigos de respuesta | Sí | No | Sí | No | No |
Manipulación de ratón | No | Sí | Sí | Sí | Sí |
Coger y arrastrar | No | Sí | No | Sí | Sí |
Acciones de teclado | No | Sí | Sí | Sí | Sí |
Visibilidad de elementos | No | Sí | No | Sí | Sí |
Evaluación Javascript | No | Sí | Sí | Sí | Sí |
Tamaño de ventanas | No | Sí | No | No | No |
Maximización de ventanas | No | Sí | No | Sí | No |
: Diferencias entre simuladores de navegadores.
8.2.1 Consideraciones sobre testeo BDD
Testear funcionalidades web utilizando Behat u otras alternativas como puede ser utilizar Selenium directamente viene asociado con algunas dificulades. Uno de los problemas que nos encontramos es el rendimiento. Testear una aplicación web abriendo un navegador, realizando un conjunto de acciones, cerrando el navegador y repetir el proceso por cada test puede llegar a ser lento. En algunos proyectos estos procesos pueden llegar a tardar varios minutos, incluso en situaciones extremas horas. Para este problema la forma de solucionarlo es utilizando herramientas de integración continua, como veremos en el capítulo dedicado a ello.
Otro problema asociado al testeo con este tipo de herramientas es el de mantenibilidad. Generalmente los tests funcionales o BDD están ligados a la estructura de los documentos HTML que genera la aplicación web para ser renderizado por el navegador. Esto quiere decir, que cada vez que hay un cambio en el documento HTML que esté asociado a un tests concreto, dicho test puede dejar de funcionar.
Otro problema viene asociado con el estado en el que la ejecución de un test deja la aplicación. A diferencia de los tests unitarios o tests de integración, los tests funcionales pueden realizar acciones de escritura en bases de datos, disco, etc. Esta situación dejar la aplicación de forma que no pueda ejecutar los tests sin producir un error. Pongamos por ejemplo un registro de usuario, en el que dicho usuario solo puede existir una vez con un mail determinado. Para la primera ejecución de los tests, funcionarían correctamente, pero para la segunda, si no realizamos alguna manipulación de la base de datos el tests no pasaría, dado que el sistema determinaría que ya existe un usuario con el mail.
Posibles soluciones a esta situación puede ser destruir la base de datos completa y recrearla con cada ejecución del test, o usar herramientas de gestión de migraciones de bases de datos.
Otra situación complicada para testeos con Behat es el envío de mails a usuarios. Enviar mails a cuentas de usuario, o de testing, cada vez que se ejecutan los tests no es lo más conveniente. Para evitar este envío de mails es conveniente utilizar librerías como FakeSMTP.
La activación de cuentas de usuario a través del envío de mail es otra situación complicada que nos podemos encontrar cuando testeamos una aplicación web. Las soluciones posibles para este problema son, o modificar la base de datos directamente o enviando un mail y extendiendo el test funcional para que se conecte por SMPT, lea el mail y active la cuenta.
8.2.2 Ejemplo de testeo usando Behat
Para ilustrar la forma en la que se usa Behat y ver los problemas anteriormente mencionados vamos a testear tres funcionalidades básicas de un blog Wordpress. Para que nuestro ejemplo sea independiente de la instalación, vamos a utilizar Docker como herramienta de contenerización de aplicaciones. Docker no entra en la temática de teste, pero para nuestro ejemplo nos ayudará a tener un blog Wordpress con las mismas condiciones para poder ejecutar el mismo test.
En pocas palabras, Docker es una herramienta capaz de crear contenedores de aplicación, ejecutándose en el sistema operativo (necesariamente Linux), pero bajo un entorno aislado del resto del sistema operativo. Para más información recomendamos visitar la web oficial: http://www.docker.com
Para simplificar la instalación de nuestro blog WordPress, Docker tiene una aplicación llamada Docker Compose, con la cual, mediante la especificación de las necesidades de aplicación en un documento Yaml y la ejecución del comando correspondiente tenemos un blog Wordpress instalado en un contenedor, aislado del resto del sistema operativo.
Para poder ejecutar este ejemplo suponemos que Docker y Docker Compose están instalados correctamente en la máquina siguiendo las instrucciones de la página oficial.
En nuestro ejemplo necesitamos el siguiente documento Yaml:
wordpress:
image: wordpress
links:
- db:mysql
ports:
- 8080:80
db:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: example
Y ejecutar el siguiente comando:
docker composer up -d
Una vez realizados estos pasos, podemos visitar http://localhost:8080 y veremos la página de instalación de nuestro blog WordPress. Para poder ejecutar los tests necesitamos realizar la instalación, que consiste en rellenar los campos Site Title, Username, Password y Email. Con esto podemos ejecutar los tests de Behat.
Hemos decidido tres escenarios para este ejemplo. El proceso de login, la creación de un post y la eliminación del post insertado. Como veremos a continuación, dado que para Behat cada escenario debe ser independiente la ejecución del login se repetirá en los tres escenarios. Como habíamos indicando anteriormente, los test se especifican en lenguaje Gherkin en un fichero formato Yaml.
Para poder ejecutar los tests necesitamos dos cosas más, el fichero de configuración propio de Behat, que debe llamarse behat.yml (también en formato Yaml). En nuestro caso su contenido debería ser:
# behat.yml
default:
extensions:
Behat\MinkExtension\Extension:
base_url: http://localhost:8080
goutte: ~
selenium2: ~
En nuestra configuración le estamos diciendo que nuestro blog tiene como url base http://localhost:8080, que es la que utilizamos para configurar con Docker nuestro blog. Además, estamos indicando que utilizaremos los clientes HTTP Goute y el simulador de navegadores web Selenium.
Nuestro fichero composer.json, el cual contendrá todas las dependencias para poder ejecutar Behat, deberá contener:
{
"require-dev": {
"behat/behat": "2.4.*@stable",
"behat/mink": "1.4.*@stable",
"behat/mink-extension": "*",
"behat/mink-goutte-driver": "*",
"behat/mink-selenium2-driver": "*"
},
"config": {
"bin-dir": "bin/"
}
}
Además de instalar las dependencias indicadas en composer.json necesitamos tener descargado y en ejecución el ejecutable de Selenium en la máquina que estamos realizando el test. "Selenium" puede descargarse de su página oficial: http://www.seleniumhq.org/download/. La versión con la que hemos realizado estas pruebas ha sido "Selenium Server Standalone 2.45.0" y el comando que hemos ejecutado en nuestra máquina ha sido:
java -jar selenium-server-standalone-2.45.0.jar
En este punto tenemos un blog Wordpress instalado y ejecutándose y un proyecto PHP capaz de ejecutar Behat y conectarse con Selenium. Lo último que necesitamos es el código Gherkin para ejecutar los tres escenarios de login, escritura de un post y eliminación del mismo. La especificación podría ser la siguiente:
#language: es
Característica: Login
Como administrador del blog Wordpress.
Necesito logarme como editor.
@javascript
Escenario: Logarme en mi propio blog
Dado estoy en "/wp-login.php"
Cuando relleno "user_login" con "jose"
Cuando relleno "user_pass" con "testing"
Cuando presiono "wp-submit"
Entonces la respuesta debe contener "Dashboard"
@javascript
Escenario: Escribo un nuevo post
Dado estoy en "/wp-login.php"
Cuando relleno "user_login" con "jose"
Cuando relleno "user_pass" con "testing"
Cuando presiono "wp-submit"
Dado estoy en "/wp-admin/post-new.php"
Entonces espero 1 segundo
Cuando relleno "post_title" con "Post de ejemplo"
Cuando relleno "content" con "Contenido de un post de ejemplo"
Cuando presiono "publish"
Entonces la respuesta debe contener "Post published"
@javascript
Escenario: Borro el post introducido anteriormente.
Dado estoy en "/wp-login.php"
Cuando relleno "user_login" con "jose"
Cuando relleno "user_pass" con "testing"
Cuando presiono "wp-submit"
Dado estoy en "/wp-admin/edit.php?post_type=post"
Entonces marco "post[]"
Entonces la casilla de selección "post[]" debe estar marcada
Entonces selecciono "Move to Trash" de "action"
Entonces presiono "doaction"
Entonces la respuesta debe contener "1 post moved to the Trash"
Dado estoy en "/wp-admin/edit.php?post_status=trash&post_type=post"
Entonces marco "post[]"
Entonces la casilla de selección "post[]" debe estar marcada
Entonces selecciono "Delete Permanently" de "action"
Entonces presiono "doaction"
Entonces la respuesta debe contener "1 post permanently deleted"
Por convenio de Behat, este fichero debe situarse en la carpeta /features del proyecto donde queramos ejecutar los tests.
Si nos fijamos en el código, es perfectamente comprensible por un humano, dado que las sentencias de ejecución son sencillas y directas. Además, la implementación de Gherkin en PHP soporta varios idiomas, no solo inglés, por lo que para nuestro ejemplo hemos podido escribir las sentencias de nuestros tests en castellano.
Behat es fácil de extender. Behat funciona por patrones: esto es, para cada formato de linea, Behat ejecuta una función propia. Por ejemplo, para una sentencia del tipo 'Dado estoy en "/wp-login.php"', Behat ejecutará una expresión regular que, si encaja, ejecutará una función interna correspondiente a esa expresión regular. En este caso le indicará a Selenium que debe abrir la página dentro del dominio base_url indicada entre comillas. Debido a ese funcionamiento, el sistema es fácilmente extensible.
Para ilustrar esta particularidad de Behat en nuestro ejemplo hemos introducido la sentencia 'Entonces espero 1 segundo', que no está en el sistema por defecto. Cuando ejecutamos Behat por primera vez la aplicación nos advierte de que esa sentencia no existe y nos propone crearla dentro de la clase propia para este efecto: FeatureContext. Para poder detener la ejecución n segundos, hemos implementado el método esperoSegundo, dentro de la clase FeatureContext de la siguiente forma:
<?php
use Behat\MinkExtension\Context\MinkContext;
/**
* Features context.
*/
class FeatureContext extends MinkContext
{
/**
* Initializes context.
* Every scenario gets it's own context object.
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct(array $parameters)
{
// Initialize your context here
}
/**
* @Given /^espero (\d+) segundos?$/
*/
public function esperoSegundo($arg1)
{
sleep($arg1);
}
}
8.3 Alternativas a Behat
Además de Behat existen otros frameworks de testing que siguen la filosofía BDD. Algunos de ellos son Pho y Peridot. En esta sección daremos una breve introducción a estos frameworks, dado que no tienen ni todas las funcionalidades que podemos encontrar con Behat, ni la aceptación por parte de la comunidad de desarrollo.
8.3.1 Pho
Pho [25] es un framework de testeo BDD para PHP, inspirado en Jasmine (framework para JavaScritp) y RSpec (framework para Ruby). Las funcionalidades son testeadas con una sintaxis similar a los dos frameworks del que se inspira y posee de forma nativa la funcionalidad "watch", la cual permite relanzar un tests al mismo tiempo que se está desarrollando.
Pho expone un lenguaje de dominio específico basado en un reducido conjunto de funciones: describe, context, in, before, after, beforeEach, afterEach y expect, cuyos significados son:
describe y context: Son intercambiables entre sí, aunque normalmente se utiliza context para indicar grupos. Se usan pasando un string como parámetro y una función con el test a realizar.
it: Define el test a realizar, es decir, las condiciones previas y los valores esperados.
expect: Define el valor que a devolver el test y las condiciones o relaciones para llegar a ese valor.
before, after, beforeEach, afterEach: Son análogos a los métodos setUp, tearDown y setUpBeforeClass y tearDownBeforeClass de PHPUnit.
Con este vocabulario, Pho puede expresar los conceptos básicos que hay bajo TDD, con una sintaxis que recuerda más a JavaScript como veremos en el siguiente ejemplo, tomado de la propia documentación de Pho:
describe('A suite', function() {
it('contains specs with expectations', function() {
expect(true)->toBe(true);
});
it('can have specs that fail', function() {
expect(false)->not()->toBe(false);
});
it('can have incomplete specs');
});
Como vemos, "describe" y context "reciben" como segundo parámetro una funcion, así como la función "it".
Al igual que vimos con Phpspec, Pho contiene un gran número de "Expectations y Matchers", los cuales pueden ser vistos en la documentación. En resumen, Pho contiene:
- Matching para tipo de datos.
- Matching para instancias.
- Matching de igualdad estricta.
- Matcihng para igual no estricta.
- Matching de longitud.
- Matching de inclusión.
- Matching de expresión regular.
- Matching numéricos.
- Matching de excepciones.
- Matching definido por usuario, el cual implementando el interfaz MatcherInterface, permite que un usuario pueda defirnir su propio matching.
Pho no contiene ningún mecanismo para trabajar con tests dobles, por lo que en la misma documentación recomiendan utilizar herramientas existentes como Prophecy o Mockery.
8.3.2 Peridot
Peridot [26] es otro framework de testeo BDD para proyectos en PHP. Los características que definen a Peridot según la documentación oficial son:
Natural: Escribir tests con la sintaxis describe-it resulta de forma natural. Fácil y claramente se pueden definir descripciones de como se debe comportar el código bajo test en un lenguaje con sentido.
Extensible: Peridot es dirigido por eventos, por lo que escribir plugins o extensiones es sencillo. Los eventos y "scopes" de Peridot permiten añadir tests a clases auxiliares, frameworks, etc.
Rápido: Peridot es ligero. Ejecutar un conjunto de tests utilizando Peridot es más rápido que PHPUnit o Phpspec. Además, Peridot permite la ejecución concurrente de los tests de forma nativa.
Para ver ligeramente algún ejemplo de cómo testear con Peridot, veamos un test obtenido de la documentación de Peridot:
describe('ArrayObject', function() {
beforeEach(function() {
$this->arrayObject = new ArrayObject(['one', 'two', 'three']);
});
describe('->count()', function() {
it("should return the number of items", function() {
$count = $this->arrayObject->count();
assert($count === 3, "expected 3");
});
});
});
El resultado de la ejecución del tests anterior sería:
ArrayObject
->count()
Ok should return the number of items
1 passing (19 ms)
Sin entrar en mucho detalle, vemos que Peridot tiene una sintaxis muy parecida a Pho. Ambos proyectos son una alternativa a Behat, aunque dada la madurez de este último no le vemos la misma utilidad para integrarlo en un proyecto que acabe con código en producción.