Primeros pasos con Test Driven Development (TDD) – Parte 1

Seguro que hemos escuchado hablar de técnicas de desarrollo como BDD (Behavior Driven Development), DDD (Domain Driven Development) o TDD (Test Driven Development). En este post haremos una pequeña introducción a la más genérica de las 3: TDD o Desarrollo Dirigido mediante Tests.

¿Por qué realizar tests?

La principal razón como desarrolladores es que los tests nos ayudan a crear código de calidad y sencillo de mantener. Aunque es cierto que puede ralentizar en cierto modo el desarrollo, nos aseguramos un código de producción donde los posibles bugs se habrán reducido a prácticamente 0.

Estos tests también nos ayudan cuando trabajamos en equipo, ya que si disponemos de una buena batería de test unitarios y funcionales, será infinitamente más fácil explicar el proyecto a un compañero: solo tendrá que leer los test para entender realmente lo que está haciendo la aplicación.

Otra característica importante es que nos permiten evolucionar nuestras aplicaciones de forma segura. Al modificar o añadir algo nuevo, nos ofrecen la certeza de no haber roto ninguna funcionalidad o, en caso contrario, nos avisan de que nuestros cambios han alterado el funcionamiento de la aplicación de manera inesperada.

No todo son ventajas, sobre todo si estamos empezando. Los tests pueden:

  • Hacer que se pierda tiempo escribiendo tests
  • Hacer que se pierda tiempo manteniendo tests
  • Pueden crear una falsa sensación de seguridad (ya que los test se pueden forzar a que pasen y no por ello significa que nuestro desarrollo cumpla con las expectativas)

Con todo esto debemos valorar si el tiempo que debemos dedicar a hacer los tests merece la pena. Por ejemplo, en aplicaciones muy pequeñas que no van a escalar, o que su vida será muy corta, es posible que no merezca la pena el tiempo de dedicación que requiere generar una buena colección de tests.

En cambio, en aplicaciones vivas, no hay duda que nos ahorrarán cantidad de tiempo, porque la mayor parte de los posibles bugs serán interceptados antes de las puestas en producción.

Tipos de tests

Test unitarios

Se encargan de probar pequeñas piezas de software de manera aislada, por ejemplo un método, o una clase en concreto. Para que podamos considerar un test como unitario debe cumplir los siguientes requisitos:*

  • No debe hablar con ninguna base de datos
  • No debe utilizar la red
  • No debe utilizar el sistema de ficheros
  • Se debe poder ejecutar en paralelo con otros tests
  • No debe requerir ninguna modificación del entorno para ser ejecutado

Si tus tests no cumplen con alguno de estos casos, simplemente significa que no es un test unitario, podría ser un test funcional o de integración.

Mocks, Stubs, Doubles…

Seguro que hemos escuchado estas palabras y es posible que no sepamos muy bien lo que significan. Cuando realizamos tests unitarios, el objeto que queremos testear (SUT o Subject Under Test), puede tener objetos colaboradores necesarios para su funcionalidad. En estos casos, utilizaremos clases de mentira que actuarán como dobles de las clases originales: a estas clases les llamaremos “Mocks”.

Tests de integración

Prueban la integración entre dos o más componentes. Por ejemplo, podrían probar el uso de una librería externa dentro de uno de nuestros métodos.

Test funcionales

Sirven para probar la aplicación desde el punto de vista del usuario final. Se pueden diferenciar de los unitarios por las siguientes razones:

  • Contiene peticiones de las que comprobamos las respuestas (Request y Responses)
  • Puede probar aspectos de navegación, como hacer click sobre un botón y comprobar el resultado

TDD

El desarrollo dirigido por tests es una técnica basada en realizar pequeños tests que describen la funcionalidad antes de desarrollarla. De esta manera, el código final debe ir consiguiendo pasar los test y avanzar mediante refactorización.

En la práctica, el TDD no se basa en realizar una enorme batería de tests y después escribir el código, sino que es preferible ir realizando pequeños ciclos de testing e ir escribiendo a su vez el código necesario para superarlos.

Cuando hacemos TDD debemos respetar las siguientes normas:

  • No debemos escribir código de producción, a menos que tengamos ya implementado su correspondiente test unitario
  • No se deben escribir más tests de los necesarios para que falle (esto significa ir uno por uno)
  • No se debe escribir más código de producción que el necesario para superar el último test fallido

Sobre estas premisas las aplicaciones que construyamos serán sólidas y escalables por su propia naturaleza. Además, nunca tendrán más código que el estrictamente necesario para cumplir con las expectativas acordadas.

Para llevarlo a cabo seguiremos la teoría descrita por James Shore en Noviembre de 2005.

Red, Green , Refactor.

Esta técnica se basa en 5 pasos:

  • Pensar: Tomarnos tiempo para pensar qué test es más adecuado para dirigirnos a la finalidad del código (este paso puede ser el más complicado mientras aprendemos).
  • Red: Escribe unas pocas líneas de código de prueba y ejecuta el test. Veremos que el test falla. Prestamos atención a los mensajes de error.
  • Green: Escribe unas pocas líneas de código de producción. En este punto no debemos preocuparnos por la pureza del diseño o la elegancia. En ocasiones, es posible “Hardcodear”, esto es correcto ya que refactorizaremos en un momento. Ejecutamos los test y veremos que son correctos.
  • Refactor: Una vez nuestros tests pasan, podemos hacer cambios sin preocuparnos de romper nada. Debemos parar un momento, mirar el código que hemos escrito y preguntarnos a nosotros mismos si podemos optimizarlo. Revisamos el código duplicado y demás “smells”. Si vemos algo que no es correcto, pero no estamos seguros de cómo corregirlo, no debemos preocuparnos: podemos revisarlo de nuevo tras finalizar el ciclo varias veces más. En esta etapa debemos tomarnos nuestro tiempo para tomar las mejores decisiones posibles. Pasamos los test y nos aseguramos de que todos sigan siendo correctos.
  • Repeat: Hagámoslo otra vez, deberíamos repetir este ciclo tantas veces sea necesario. Normalmente lo repetiremos de tres a cinco veces tan rápido como sea posible, entonces veremos que necesitamos dedicar más tiempo a refactorizar. Es importante lograr agilidad repitiendo este ciclo: con práctica podemos llegar a repetirlo en 20 ocasiones por hora aproximadamente.

En palabras de James Shore, este proceso funciona bien por dos razones. La primera es que vamos trabajando sobre pequeños pasos, formulando y comprobando hipótesis constantemente (los tests deberían estar en rojo…, ahora deberían cambiar a verde…, rojo…, verde…). En el momento que cometamos un error será muy sencillo identificarlo, ya que tan solo hemos escrito unas pocas líneas desde el estado anterior. Todos sabemos que localizar y arreglar fallos es una de las partes de nuestro trabajo que más tiempo requiere.

La otra razón que hace que este proceso funcione, es porque nos obliga a estar siempre pendientes del diseño, ya sea decidiendo el siguiente test, la estructura a seguir o cómo refactorizar. De modo que al probarlo inmediatamente, veremos rápidamente si nuestro diseño es bueno o no.

Manos a la obra

Después de un poquito de teoría, pasemos a la práctica. Crearemos un entorno de desarrollo mediante testing en PHP, configuraremos nuestro proyecto mediante composer y le añadiremos PHPUnit*. Después decidiremos qué framework utilizar para desarrollar una mini app de presentación de libros.

Configurando el entorno

Lo primero que necesitamos para nuestro entorno es Composer. Como ya explicó nuestro compañero Dani en “Primeros pasos con Composer”, es el gestor de paquetes que utilizamos en php y daremos por hecho que ya lo tenemos instalado como binario en nuestra máquina.

Composer

Para ello abrimos una consola y nos situamos en nuestro workspace donde crearemos el directorio donde se alojará nuestra aplicación.

Lo primero que necesitamos para nuestro proyecto será el archivo composer.json

Con el siguiente contenido como mínimo

Simplemente estamos dando de alta el namespace dentro del directorio core/. Para generar el autoloader ejecutaremos el siguiente comando:

Este genera la carpeta vendor dentro de nuestra raíz donde se alojarán todas la librerías que requiramos mediante composer. Es recomendable excluir de git este directorio.

Añadimos la siguiente línea:

y realizamos nuestro primer commit

Podemos seguir el tutorial desde el propio repositorio de Github paso a paso , siguiendo los tags.

PHPUnit

El siguiente paso será configurar phpunit para utilizarlo en el desarrollo de nuestra aplicación, primero lo requerimos con composer

Una vez instalado, necesitamos crear el archivo de configuración phpunit.xml.dist

con el siguiente contenido como mínimo

Comprobamos:

Commiteamos de nuevo nuestra tarea.

Test mínimo viable

Como podemos ver en la primera ejecución de phpunit recibimos un mensaje de respuesta, que nos informa que no hay ningún test listo para ejecutar, lo solucionamos rápidamemte creando el directorio tests en la raíz del proyecto y una nueva Clase con el sufijo “Test”, por ejemplo, InitialPageTest.

Con el siguiente contenido

Si ejecutamos de nuevo nuestros tests veremos que ahora si disponemos de un test, y que además es válido.

La aplicación

Crearemos una simple aplicación en la que mostraremos un listado de libros acorde a ciertas categorías, los user storys serían los siguientes:

  • Al acceder veremos un listado de libros de todas las categorías
  • Los libros se pueden filtrar por su categoría, el listado solo debe mostrar los libros que dispongan de dicha categoría.

Modelo

Para empezar necesitaremos crear nuestras entidades, esta parte podríamos no testearla, ya que tan solo dispondrá de Getters y Setters de sus propiedades, a los que daremos cobertura mediante el testing de los demás componentes. Aun así crearemos un simple test para cada entidad.

 

Si ejecutamos de nuevo los tests, obtendremos un mensaje muy explícito de respuesta.

 

Esta respuesta no da la información necesaria para continuar con el desarrollo; La clase TDDIntro\Domain\Entity\Book no existe, por lo tanto vamos a crearla.

 

si volvemos a ejecutar los tests veremos todo en verde, continuamos.

Ahora pasaremos a probar las categorías

 

Es obvio que en la siguiente ejecución, los tests no pasarán, por lo que vamos directamente a crear la entidad Category.

 

Con la siguiente clase creada, volvemos al verde. Ahora pasaremos a crear la relación entre Libros y categorías. para ello modificaremos el archivo BookEntityTest.php, nuestro método testBookEntity, debe quedar de la siguiente manera.

 

Actualizamos la entidad:

y pasamos de nuevo los tests y si todo ha ido bien volvemos a nuestro deseado verde. Este podría ser un buen momento para commitear nuestra tarea.

Lógica de negocio

Para evitar el acoplamiento de nuestra aplicación con la capa de persistencia, implementaremos el patrón repositorio.

Este repositorio debe tener como mínimo dos métodos findAll y findBy, y debemos decidir el motor de persistencia a utilizar, en este caso utilizaremos Doctrine ORM, que nos facilita la utilización de motores de BBDD tipo SQL, como mysql o sqlite.

Antes de nada definiremos los contratos “Interfaces” que nos ayudarán con nuestra tarea y nos permitirán implementar diferentes tipos de persistencia en caso de ser necesario.

Primero creamos una interfaz genérica de la que extenderán las demás.

 

Ahora creamos el BookRepository y el categoryRepository.

 

y

 

Este es un buen momento para commitear.

Creando la aplicación

En este punto, disponemos de un sólido modelo de datos (con 100% de cobertura), por lo que es el momento para tomar varias decisiones:

  • Qué framework utilizaremos
  • Qué patrones de diseño utilizaremos
  • Qué componentes utilizaremos

Uno de nuestros objetivos como programadores de código de calidad es lograr la abstracción posible de del framework que decidamos utilizar, es decir, que nuestro framework no sea quien condicione nuestro desarrollo, sino que lo utilicemos como una herramienta de ayuda, que en un momento dado en el tiempo pueda ser intercambiable sin necesidad de modificar la lógica negocio.

A la hora de seleccionar un framework, nos centraremos en las funcionalidades que sabemos que necesitamos, por ejemplo, el componente principal que utilizaremos(independientemente del framework) será la inyección de dependencias, Pimple puede ser una buena opción, Doctrine para la capa de persistencia, twig en las plantillas y los componentes “HttpFoundation”, “Router” y “Console” de Symfony.

El micro framework Silex cumple con todos los requisitos, por lo que es perfecto para nuestra aplicación.

Pongámoslo a punto:

Creamos el archivo index.php que actuará como controlador frontal de nuestra aplicación:

 

El archivo index.php requiere del archivo app.php, donde cargaremos la configuración rutas, etc.

 

Creamos los archivos requeridos:

Más adelante entraremos en detalle.

Actualizamos el composer.json

Para seguir con TDD crearemos un entorno de testing.

Como podemos ver es prácticamente idéntico al entorno de producción, a diferencia del archivo parameters.php, que lo hemos cambiado por parameters_test.php.

Ahora creamos un test base para nuestros controladores:

Actualizamos el archivo phpunit.xml.dist para ejecutar nuestro entorno de test:

Por último, para habilitar los tests funcionales en Silex añadimos el componente “Browser-Kit” de Symfony.

Regeneramos el autoloader:

y comprobamos que todos los test siguen funcionando correctamente.

Si es así, commiteamos y continuamos.

Primer user story

1. Al acceder veremos un listado de libros de todas las categorías

Creamos un nuevo archivo con un único test funcional (de momento), este describirá el comportamiento que queremos para el controlador de nuestra página principal.

Si pasamos los test encontramos un error 404, no existe la ruta “/”, vamos a crearla.

Para mantener el código ordenado, los controladores los situaremos en archivos separados, para hacer esto en Silex necesitamos incluir el componente “ControllerServiceProvider”.

Creamos el controlador con lo mínimo para pasar el test.

Si los test son correctos podemos commitear y continuar.

En el controlador hemos generado la estructura de datos que esperamos recibir y la vista, ya que de momento no tenemos ningún motor de plantillas. Como hemos previsto al principio utilizaremos Twig, vamos a instalarlo y refactorizar nuestro controlador.

Añadimos el provider de Twig a nuestro providers.php

Añadimos “Twig” como dependencia.

Creamos la nueva vista:

Actualizamos el controlador para renderizar la vista mediante Twig.

Necesitamos inyectar Twig en el controlador mediante pimple.

Y actualizamos el routes.php.

pasamos de nuevo los test y deberíamos seguir en verde, commiteamos.

Para terminar de refactorizar el controlador nos falta comunicar el framework con el modelo. Hora de retomar nuestro repositorio.

Para conectar nuestro el controlador con las entidades utilizaremos el método findAll() del repositorio BookRepository, volvamos a los tests.

 

Si ejecutamos los test veremos que estamos intentado instanciar métodos de las clases EntityRepository y TDDIntro\Persistence\Doctrine\BookRepository, que todavía no existen en el proyecto. Primero instalaremos doctrine, después crearemos una clase abstracta de la que extenderán los demás repositorios para evitar duplicar código.

Doctrine

Para integrar Doctrine con Silex utilizaremos el proveedor oficial de Silex y “Doctrine ORM Service Provider” de “dflydev”, instalamos las dependencias

Abrimos el archivo de configuración creamos la clave “doctrine” dentro del array $config, en la clave “dbal” seteamos los datos de conexión a la base datos y el driver para las consultas, por otro lado en la clave “orm” ponemos las opciones de mapeo de nuestras entidades; el tipo de mappeo, el namespace donde se encuentran las entidades y la ruta al directorio donde pondremos los archivos de mappeo.

 

Para seguir con los tests funcionales puede que utilicemos una bbdd sqlite con datos fijos, así que abrimos el archivo de parameters_test.php y lo actualizamos

Y añadimos los “Providers”

En este punto, podemos commitear.

Seguimos con los archivos de mapeo en la ruta que hemos puesto en la configuración.

La entidad Category.

Y la entidad Book que tendrá una relación “N a M” entre sí misma y categorías

No hemos definido ninguna estrategia de generación de identificadores para facilitar el testeo, esto significa que debemos conocer los “ids” de antemano.

Actualizamos las entidades para que soporten identificadores, para no repetir código podemos definir una clase abstracta y extender nuestras entidades de ella. Modificamos los tests para añadir el nuevo requisito.

Y modificamos las respectivas entidades.

La entidad Book

La entidad Category

Si pasamos los tests, vemos que seguimos en rojo, nos falta crear la clase BookRepository, esta a su vez extenderá de la clase EntityRepository de doctrine y será una implementación de la clase BookRepositoryInterface. Primero crearemos la clase AbstractRepository donde situaremos los métodos compartidos entre todas las entidades.

El método findBy lo añadimos para complir con las espectativas de nuestros interfaces, creamos también la clase BookRepository.

Pasamos los test… y vemos que volvemos al verde, hacemos un commit y continuamos.

Symfony Console

Para sacar mayor provecho de Doctrine ORM implementaremos nuestra propia herramienta de consola como no con el componente “Console” de Symfony.

Es muy sencillo solo necesitamos el ejecutable y notificarle los comandos que tiene disponibles.

y por último el archivo commands.php donde indicamos los comandos que tendremos disponibles

Configuramos el nombre y la ruta del CLI a Silex

Volvemos al archivo de proveedores

Ahora ya tenemos el CLI operativo con todos los comandos que trae Doctrine DBal y Doctrine ORM, además podemos ejecutar diferentes entornos gracias a la opción “-e”, con el entorno de test por defecto. Comprobamos que la consola funciona correctamente ejecutándola con la opción “–help”.

Veremos todos los comandos disponibles y las opciones globales. Un momento, es demasiado largo, crearemos un symlink para tenerla en sitio más estandarizado, modificamos “composer.json”

Hacemos “composer install” y volvemos a ejecutar el comando de ayudan pero desde “bin/console”

Podemos comprobar que nuestros archivos de mapeo son corrector con el comando “orm:validate-schema”

En el entorno de test podemos crear la Base de datos y sus tablas con el comando “orm:schema-tool:create”

Debemos seguir viendo todo correctamente, así que es momento de commitear.

Conectando los puntos

Ahora tenemos por un lado el repositorio, por otro el controlador usaremos la inyección de dependencias para unirlos. Modificamos el controlador:

Modificamos el archivo services.php

Si pasamos los tests de nuevo veremos que nuestro controlador no cumple las espectativas, donde esperaba dos elemento no ha encontrado ninguno. Esto es porque o tenemos nada en nuestra base de datos de test. Podemos insertar los los registros a mano en sqlite, o crear un FakeBookRepository que nos entregue valores predefinidos por nosotros.

Para esto crearemos dos nuevos archivos BookFixtures que imitará los datos de la bbdd y FakeBookrepository que imitará el Gateway.

nuestro repositorio “fake”.

Creamos un archivo services_test.php que sobrescribirá el services.php original.

y por último modificamos el archivo app_test.php

Pasamos los test y volvemos al esperado verde, esto último paso añade mucho mayor valor a nuestros test, que insertar los datos directamente en la BBDD, ya que demuestra la versatilidad de nuestro código, cambiar el sistema de persistencia sería tan sencillo como añadir una nueva implementación de nuestro BookRepositoryInterface. Es momento de commitear y pasar a nuestro segundo user story.

En este post hemos visto los principios del TDD y como aplicarlo, además hemos implementado una sólida arquitectura, que podría ser el principio de cualquier proyecto a largo plazo, gracias a la cobertura de test. En la segunda parte terminaremos el segundo user story, por el momento podemos seguir paso a paso el tutorial siguiendo el repositorio de git



¿Te gusta este post? Es solo un ejemplo de cómo podemos ayudar a tu empresa...
Sobre Koldo Picaza

Soy un programador PHP web con más de 4 años de experiencia, sobre todo como desarrollador Symfony y Drupal. Tengo aptitudes para crear módulos y bundles personalizados integración con software de terceros o webservices a Drupal o symfony. Soy una persona apasionada por la programación, pro-activa, trabajadora y comprometida.

Queremos tu opinión :)