Testing automático en nuestros proyectos PHP con Docker, Behat y CircleCI

Carlos Revillo
The Cocktail Engineering
6 min readMay 3, 2018

--

En The Cocktail estamos apostando por implantar testing automático en nuestros proyectos con el fin de mejorar nuestras entregas. En este artículo os contamos cómo lo estamos haciendo en uno de los proyectos que estamos desarrollando actualmente.

Docker

Docker nos ayuda a tener distintos entornos de desarrollo en función del proyecto en el que estamos trabajando. En ocasiones, debemos seguir manteniendo desarrollados hechos hace algún tiempo y cuya infraestructura (versión de sistema operativo, php, mysql…) es distinta a la que usaríamos si empezáramos a trabajar en un proyecto nuevo.

El objetivo: Que cada persona del equipo pueda trabajar con un entorno lo más parecido posible al que usaremos en producción y que ese entorno sea el mismo para todas estas personas.

Para ello contamos con la ayuda de nuestro departamento de sistemas. En función de los requisitos del proyecto, ellos generan una serie de imágenes. Esas imágenes más otras que obtenemos desde el hub de docker las orquestamos vía docker-compose.

Como ejemplo, este el docker-compose.yaml que estamos usando para el proyecto del que os hablamos en este artículo. Usamos Symfony 4 como framework. A nivel de infraestructura usamos PHP-FPM, Nginx como servidor web, MySQL como servidor de base de datos y Redis.

En el ejemplo hemos ocultado alguno de los valores reales. Puedes ver que nuestro docker-compose define 4 servicios. Los dos primeros, fpm y nginx, parten de imágenes que crea nuestro equipo de sistemas. Lo hacemos así para evitarnos el trabajo adicional de tener que añadir librerías PHP o algún módulo adicional necesario para el NGINX. En ambas se definen “commands” , que sirven para traernos las últimas dependencias, compilar assets y otras en el caso de la imagen fpm o para generar virtual hosts en la imagen de nginx

Por su parte, las imágenes de MySQL y Redis son imágenes que descargamos desde el Docker Hub.

Con este punto de partida, cualquier persona puede entrar al equipo y tener disponible un entorno con el que trabajar idéntico al del resto de integrantes del mismo.

Este docker-compose.yml puede evolucionar. En una de las funcionalidades a implementar se necesitaba un envío de correos. En producción, este envío de correos se realizará a través de un servidor SMTP, pero no suele ser buena idea que ese mismo servidor se use desde desarrollo donde quizás lo mejor es que los correos nunca se envíen.

Para solventar esto solemos meter una imagen adicional en el docker-compose. Es una imagen de mailcatcher.

Con esta configuración, todas las personas del proyecto dispondrán de un interfaz web en el que podrán comprobar la correcta generación de los correos electrónicos. Y la tienen sin necesidad de trabajo adicional a nivel de instalación de software en sus máquinas.

CircleCI

Aunque esto puede cambiar en un futuro no muy lejano, de momento estamos usando CircleCI como entorno de integración continua. Una de las primeras ventajas que obtenemos es que ese mismo docker-compose que usamos en nuestro entorno de desarrollo podemos usarlo en CircleCI.

Para generar estos builds necesitamos crear un archivo .circle/config.yml en nuestro repositorio y en él le pedimos lo siguiente:

  1. Que haga un checkout desde nuestro repositorio de la rama que queremos testar
  2. Que use docker-compose de la misma forma que lo haría cualquier persona que entrase por primera vez al proyecto
  3. Que ejecute una serie de tests.

Este es el config.yml que tenemos en nuestro repositorio y que le sirve a circle para saber qué tareas realizar y en qué orden:

En los primeros pasos es donde le decimos que haga el checkout de nuestro código. Además, con el fin de acelerar el proceso, le decimos que si nuestro composer.lock no ha cambiado, restaure las dependencias del proyecto desde una caché.

Luego, con ./dockerbuild.sh construimos alguna de las imágenes en local y usamos docker-compose up para levantar el entorno.

En los últimos tres pasos es donde ejecutamos los tests.

  • Usamos phpcs para comprobar que nuestros archivos PHP respetan el standard PSR2
  • Ejecutamos tests unitarios con PhpUnit.
  • Ejecutamos tests de integración con Behat. Aquí, y también con el fin de acelerar el proceso, le decimos a circle que use una base de datos en memoria para ejecutar estos tests.

De nuevo, usar docker aquí nos evita tener que añadir al build dependencias de forma manual.

Behat

Como os decíamos antes, nuestro proyecto se está desarrollando usando Symfony 4 como framework. Una de las funcionalidades requeridas es bastante común: Que un usuario pueda recuperar su contraseña si se le ha olvidado.

Parece una tarea sencilla en cuanto a funcionalidad. Los usuarios deberían poder acceder a un formulario donde, tras introducir su correo electrónico, deberían recibir en su buzón un mensaje que contiene un link. Este link tiene una validez temporal. El click sobre él deberá devolver al usuario a una zona de la web donde podrá elegir su nueva contraseña.

Entonces, sabiendo que desde nuestro entorno de desarrollo los correos van a ser “interceptados” por mailcatcher, ¿cómo podemos asegurarnos de que lo desarrollado da servicio a esta funcionalidad?. Y, sobre todo, ¿cómo nos aseguramos de que cualquier cambio que hagamos de aquí a X meses no va a añadir una regresión a este código?

No es descabellado pensar que siendo una funcionalidad sencilla una prueba “manual” pueda servir. Y seguramente así sea. Ahora bien, ¿vamos a realizar esta prueba manual cada vez que introduzcamos cualquier otro cambio en el proyecto? No parece la mejor idea.

Es aquí donde el testing automático cobra vital importancia. En nuestro caso hemos creado una feature para esta funcionalidad de la que extreamos algunos escenarios.

El primero es el más sencillo. Cuando un usuario va la página de login, debería poder ver un enlace para recuperar su contraseña.

Un test tan sencillo como importante. Por ejemplo, ¿qué ocurriría si a alguien, más adelante, se le escapa un carácter justo en la parte de traducciones dónde se define esa cadena?. Este test fallaría y evitaría que subiéramos a producción un texto que no queremos subir.

¿Qué ocurre cuando un usuario introduce un email que no está presente en la base de datos? ¿Está preparada nuestra aplicación para decírselo? Probémoslo.

Envío de correos

Las especificaciones nos decían que había que enviar un correo al usuario en el caso de que su email si existiese en la base de datos. ¿Cómo comprobar que ese mail se envía y que el cuerpo de ese mensaje responde a lo que queremos?

Recordemos que queremos hacer nuestros builds en circle y que en lo que allí montamos tampoco hay ningún servidor de correo que podamos utilizar.

Para estos casos, un “truco” es utilizar el profiler de Symfony. Cuando se usa el servicio de envío de correos de Symfony, éste loga la información relativa a ese correo a modo de logs. Podemos por tanto acceder al profiler y allí inspeccionar si existe un correo que responde a las necesidades.

Lo primero que hemos de hacer es marcar nuestro escenario con un tag.

Si bien behat y sus extensiones nos ofrecen definiciones de pasos comunes a todos los proyectos, será habitual que nosotros tengamos que definir más pasos. En este caso, para probar esta funcionalidad lo primero que debemos suponer es que existe un usuario en la base de datos con determinados datos. En este caso, un email.

Del escenario anterior, este sería el primer paso.

Partimos de una base de datos vacía. Creamos un grupo de usuarios y luego un usuario de ese grupo.

En el segundo paso, accedemos a ese usuario y le asignamos un email, de nuevo usando el EntityManager de Doctrine.

Finalmente, debemos definir un paso para comprobar que el correo ha sido enviado al destinatario correcto y que tiene un link de recuperación de contraseña. Estos links tienen la ruta /change-password/ seguida de un token de 32 caracteres alfanuméricos.

En este método obtenemos el profiler de Symfony. De él extraeremos lo relativo a “swiftmailer”, que es el servicio encargado del envío de correo. Hacemos un loop sobre todos los mensajes que aparezcan. Cuando el destinatario del mensaje coincide con el que queremos, miramos, a través de una expresión regular, que ese mensaje contenga un link de recuperación de contraseña.

Caso de no tener un enlace en el correo que cumpla esa expresión regular se lanzaría una Exception y por tanto el test sería dado como fallido.

Conclusiones

Es innegable que montar estas cosas así como escribir estos tests lleva cierto tiempo. Sin embargo, ese tiempo usado en esta fase nos está proporcionado bastantes mejoras en nuestros desarrollos

  • Se han acabado (o casi) los famosos “En mi local funciona
  • Todo el mundo escribe código PHP de forma similar, siguiendo un standards determinados
  • Mayor seguridad a la hora de escribir nuevas funcionalidades. Nos preocupamos menos de escribir pensando en si podemos estropear algo que haya escrito otra persona y desconocemos. Si esto ocurre, serán los tests quien nos lo diga y actuaremos entonces, no antes.
  • El tiempo de inmersión de las personas en los proyectos se reduce ostensiblemente. Se evita la necesidad de instalar librerías o software adicional en cada ordenador de cada desarrollador o desarrolladora.

--

--