A lo largo de la historia del software han ido surgiendo diferentes metodologías de trabajo. Pero centrándonos en las mas actuales, encontramos la Programación orientada a objetivos como una de las mas usadas. Seguida de ella tenemos otras como TDD (Test Driven Design), BDD (Behavior Driven Design) y en última instancia, no por ello menos importante, DDD (Domain Driven Design).
El objetivo de estas metodologías no es otro que escribir mejor código para permitir a la aplicación mantenibilidad, bajos costes y extensibilidad, entre otros. El último, está cobrando fuerza en los últimos años con la proliferación de entornos como Kubernetes, que nos permiten tener una separación virtual entre nodos, sin tener que recurrir a una separación física entre servidores, como se venía haciendo hasta el momento.
Que es el testing y porqué deberíamos realizarlo
Tabla de contenidos
ToggleUna de las partes mas importantes del desarrollo de software es tener un buen diseño. Pero existen otras técnicas que nos permiten dotar de mayor fiabilidad a nuestra aplicación, como es el caso de la escritura de Test a diferentes niveles. Nos permiten acotar los casos de fallo bajo diferentes situaciones. La mayoría de las veces, no vamos a contemplar el 100% de los casos, ya sea por errores humanos, falta de análisis de todos los edge cases o por integraciones con servicios de terceros. Este aspecto se vuelve bastante necesario, sobretodo cuando queremos tener algún mecanismo de integración continua, puesto que nos puede ayudar a prevenir posibles problemas, antes de llegar a desplegar la nueva lógica en productivo entre otros.
Existen muchas tipologías en función de la tecnología, pero, a grandes rasgos, podemos encontrar 3 tipos de test entre los cuales encontramos los unitarios, los de integración y los E2E. Los test unitarios, son la base de la pirámide. Son fáciles de escribir y de mantener. Resultan muy útiles para testear partes unitarias de nuestro código, como su propio nombre indica. Siempre es recomendable tener una base sólida de test unitarios que nos permitan realizar cambios en nuestra aplicación para evitar posibles side-effects. Por otro lado, tenemos los de integración. Resultan mas costos en tiempo de desarrollo y de mantenimiento. Lo que nos permiten es testear el sistema en conjuntos o grupos, cosa que no podemos hacer con los unitarios. Por último tenemos los E2E o UI testing. Estos son lo mas cercano al usuario. Aquí vamos a hacernos pasar por un usuario real, accediendo a la web, e interactuando con ella. Estos test son muy costosos de realizar y de mantener, puesto que tenemos que buscar patrones que nos permitan identificar el correcto funcionamiento de la plataforma. Entre las tareas mas comunes que pueden realizar están, la de pulsar sobre enlaces, botones o mandar formularios.
TDD
Vamos a usar TDD de forma muy básica para realizar los ejemplos de código, la cual cosa nos va a forzar a trabajar con la creación de test antes de la implementación. Es importante destacar, que el diseño de la característica que queramos desarrollar debe estar planificado de antemano, son cosas complementarias, no excluyentes. Existen diferentes herramientas en PHP para realizar test, como PHPSpec si queremos hacer BDD o PHPUnit. En nuestro caso vamos a usar esta última para realizar el desarrollo del ejemplo usando TDD de forma muy básica. Las 3 leyes que Robert C. Martin describe al usar esta metodología, son las siguientes:
- No está permitido escribir ningún código de producción a menos que haga pasar un test unitario que falle.
- No está permitido escribir más de un test unitario que sea suficiente para fallar; y los errores de compilación son fallos.
- No está permitido escribir más código de producción del que sea necesario para hacer pasar un test unitario que falle.
Es importantes destacar el flujo de trabajo que vamos a seguir, en lo que se conoce como Red-Green-Refactor. Nuestro primer paso siempre es hacer un test que falle, como indica la 1º regla. Es en este momento cuando podemos pasar al segundo paso. Hacer que el test pase, en la mayoría de entornos de test esto se indica con verde. Hay que tener en cuenta, que como indica la 2º regla, nuestro propósito es única y exclusivamente hacer pasar el test. Hasta ese momento no podemos llevar a cabo ninguna acción mas. Esto nos va a forzar a seguir siempre ese proceso, de lo contrario, podríamos asumir el resultado viendo la simplicidad del test y provocar problemas mayores en partes complejas de un problema del cual no conocemos todos sus edge-cases. Cuando tenemos la 2º fase terminada, es cuando podemos refactorizar la lógica y/o test hasta que quede acorde al diseño previamente deseado. Lo que se busca de esta forma es ir de afuera hacia adentro, buscando los casos mas comunes a los edge-cases mas complejos. Hay que tener en cuenta que no debemos escribir nunca un test que ya esté contemplado en alguno previamente creado.
Caso de uso
En primer lugar vamos a instalar phpunit para empezar a usarlo de la siguiente forma:
También lo podemos instalar en formato de ejecutable (PHAR) para que no dependa de nuestro proyecto, como explican en la documentación. Si lo instalas de esta forma, es importante que configures el autoloading de
composer de forma manual como explican en el siguiente enlace.
Ahora lo que vamos a hacer es crear el archivo de configuración de PHPUnit con un comando que nos provee:
Como se observa en la imagen, tendrás que indicarle en que directorios se encuentran el autoload.php de composer, el directorio de tests y el de src. En nuestro caso lo hemos dejado por defecto, porque vamos a usar esa estructura de directorios:
Nuestro primer paso va a ser crear el test para nuestro sencillo objeto Queue donde meter elementos. La forma de ejecutar los test, en nuestro caso siempre va a ser la misma.
Aquí hemos añadido el flag «–colors» para ejecutar el test, queremos ver con claridad ese color rojo tan característico de nuestra primera fase del flujo.
Ahora vamos a crear la lógica mínima para que el test pase de rojo a verde. Te animamos a que investigues para que puede servir esa anotación covers.
Cabe resaltar, que ni tan siquiera hemos añadido constructor, queremos la mínima expresión que haga verde nuestro test. Ahora vamos a volver a ejecutarlo para ver el resultado:
Ya tenemos nuestro primer test en verde (hemos añadido el «use» de la clase Queue al test para que funcione, evitamos añadir el snippet por redundacia), en este caso, nos vamos a saltar el proceso de refactor, puesto que no queremos añadir ningún requerimiento al construir el objeto.
Como siguiente feature, lo que vamos a hacer es dotar a nuestro Queue de dos nuevos comportamientos, añadir un elemento, y obtener todos los elementos. Para ello vamos a hacer uso de una interfaz, queremos que nuestros compañeros implementen su propio objeto Queue según sus necesidades, pero no que modifique nuestra implementación. Volvemos al flujo comentado anteriormente, creamos un nuevo test:
Como podrás observar, el test está rojo, es el momento de convertirlo en verde, para ello vamos a crear la interfaz y hacer que nuestro Queue la implemente (como hemos hecho anteriormente con la clase, incluimos el use en el test y creamos una interfaz vacía):
Con esto hemos conseguido lo que nos proponíamos, hacer pasar el test. Lo siguiente que vamos a hacer es dotar a la interfaz de los comportamientos que hemos comentado anteriormente. Recordemos el proceso, crear el test y acto seguido incluir la implementación. Nosotros vamos a incluirlo en ese orden todo a la vez, puesto que ya conocemos el proceso y queremos mostrarlo todo de forma mas compacta:
Recuerda que solo necesitamos hacer pasar los test, hasta ese momento no podemos implementar lógica, por eso estamos pasándole al método add un string vacío.
Si todavía no lo has hecho, es el momento de que ejecutes los test en modo –testdox. Vamos a ver el sentido que tiene poner nombres con significado a cada uno de los test.
Ahora vamos a implementar la lógica pero con un test, recuerda el proceso Red-Green-Refactor, es muy importante:
Te dejamos algunos consejos a la hora de crear tests:
- Intenta evitar argumentos con valores primitivos, extráelos a constantes para dotar de mayor legibilidad al código.
- Nosotros hemos usado la abreviación sut para hacer referencia a SubjectUnderTest.
- Puedes hacer uso del assert null para casos en los que el método no tiene valores de devolución.
Conclusión
Hemos hecho un breve recorrido de como testear un pequeño ejemplo siguiendo la metodología TDD. Es importante que te adhieras a la metodología que mejor se adapte a las necesidades del negocio, contando con el apoyo del equipo, para dotarlo de una mayor cohesión. Nos hemos dejado cosas importantes, que te invitamos a investigar, como la cobertura de test. PHPUnit tiene una pequeña funcionalidad para generar informes en gran variedad de formatos. Con esto podemos detectar las partes con menor cantidad de test para solventarlo. Es interesante usarla, pero el 100% de cobertura no nos asegura que no exista ninguna inconsistencia en nuestro nuestro código, es una medida mas de la calidad de los test, hay ponerla en contexto con el resto de técnicas.
Por otro lado, hay herramientas que nos permiten realizar mutaciones en los test, descubriendo posibles partes débiles o mal testeadas, Infection es una librería muy interesante. Realizando mutaciones sencillas, es capaz de encontrar casos que previamente no se había contemplado. Si te interesa, puedes usar los mokcs que proporciona PHPUnit o librerías como Mockery. La unión de ambas técnicas, el code coverage con los mutation test, provee de una base de test unitarios muy sólida y bastante recomendable para seguir desarrollando la arquitectura del código. Siempre hay que realizar una evaluación a posteriori de todos los casos de uso de nuestra aplicación.