Primer acercamiento a tests
2.1 ¿Por qué, cuándo y cómo hacer tests?
Testear un proyecto de software es una garantía. Michael Feathers en define legacy code [8] como aquel fragmento de código que carece de test. Un conjunto de tests relativo a un fragmento de código expresa el contrato escrito de ese código. Los beneficios de testear están ampliamente extendidos en prácticamente todas las comunidades de software y son fácilmente contrastables. Por una parte al tener un conjunto de tests que verifiquen el comportamiento de determinado software, futuros cambios mantendrán tal funcionamiento, o indicarán, a través de la ruptura de test, que dicho comportamiento ha cambiado. Esto es lo que llamamos test de regresión.
Además de la estabilidad a la hora de desarrollar, el hecho de seguir patrones de testeo obligan al desarrollador a pensar en otras buenas prácticas para facilitar la testeabilidad del código. O dicho de otra forma, un código testeado necesariamente es mejor, según métricas de calidad de código, que otro no testeado. Algunas de estas recomendaciones o exigencias por parte de la testeabilidad son:
Principio de responsabilidad simple: Esto quiere decir que una clase debe ser responsable de realizar una única tarea. De esta forma, el código estará mejor estructurado y será más fácil de testear.
Clases con un número de lineas tendiendo a pequeño. Es más fácil de testear una clase de 200 a 300 lineas con un conjunto de métodos públicos coherentes que no clases con miles de lineas y múltiples responsabilidades, también llamadas "Clases Dios". Estas "Clases Dios" es un antipatrón conocido y debe ser evitado.
Métodos pequeños: Testear métodos de 10 a 20 líneas debe ser relativamente sencillo, dado que la complejidad ciclomática de dichos métodos tiene que ser reducida.
Desacoplamiento: Cuando nos referimos a test unitarios, desacoplar el código hace posible el testeo, por lo cual algunos entornos de desarrollo facilitan inyectores de dependencias para aislar las responsabilidades de cada pieza de código facilitando el testeo.
2.2 ¿Cuándo testear?
En función de cuando realizamos los tests de nuestro desarrollo, podemos hablar de tres formas.
2.2.1 Antes
TDD (Test-Driven Development, o en español, Desarrollo guiado por tests) es una metodología que recomienda realizar los test antes de escribir el código que pasaría dicho test. La comunidad defensora de hacer TDD asegura que de esta forma se diseña mejor la aplicación y se garantiza una cobertura de test dado que el test está implementado.
2.2.2 Durante
Mientras se va desarrollando, de forma complementaria al desarrollo en sí, programar y ejecutar los tests ayudan a mantener la calidad de código como mencionábamos anteriormente y a asegurar el funcionamiento del software tal y como la especificación determina.
2.2.3 Después
Seguir escribiendo tests tanto para modificaciones futuras como para solución de bugs detectados a posteriori, son una buena práctica a mantener. Por una parte, a mayor cobertura de tests mayor confianza se debe tener en que seguirá el correcto funcionamiento. Por otra parte, al detectar un error, resolverlo y añadir el test que previene que ese bug no vuelva a suceder en el futuro. Esto mejora la calidad del conjunto de tests y la estabilidad del proyecto.
2.3 ¿Siempre se puede puede testear?
Aunque la respuesta parezca afirmativa, en algunas situaciones puede resultar casi imposible testear. Estas situaciones normalmente ocurren cuando la calidad de código es muy pobre, sin diseño, apenas orientación a objetos y malas prácticas a cualquier nivel de código. Frente a este tipo de situaciones introducir testing suele ser bastante complicado, aunque no imposible. Realizar tests en situaciones así es análogo a rediseñar y refactorizar el código, con la complejidad que refactorizar un proyecto sin tests conlleva. La probabilidad de introducir nuevos errores o modificar el comportamiento de la aplicación en aplicaciones sin tests es bastante alta.
Siempre que se aborde a una refactorización de código, debemos tener un conjunto de tests para que el resultado de la refactorización sea el mismo que el previo a tal refactorización.
2.4 Tipos de test
Según las dependencias con otros sistemas o partes del paquete o software, los tests se pueden clasificar de la siguiente forma.
2.4.1 Unitarios
Testean la más pequeña unidad funcional, generalmente un método público o una función. El test unitario debe de centrarse únicamente en probar el comportamiento de dicha función y además debe residir en memoria. Para ello, un test unitario no deberá nunca acceder a otra funcionalidad o tener dependencia de otra parte del sistema, como pueden ser: otra clase del mismo paquete, acceso a red, acceso a base de datos, uso de ficheros de disco o ejecutar otro proceso. Cualquier tipo de dependencia con estos u otros sistemas deberían de simularse mediante mocks, stubs,... De este tipo de objetos hablaremos en los próximos capítulos.
2.4.2 Integración
Un test de integración prueba un conjunto funcionalidades de forma conjunta. Este tipo de test es responsable de verificar que el resultado de la ejecución del sistema a testear es el esperado. Los tests de integración se diferencian de los tests unitarios en que pueden acceder a disco, base de datos, red, etcétera, con el objetivo de encontrar errores que los tests unitarios no pueden detectar por sí solos. Debido a esto, los tests funcionales requieren de un entorno de tests lo más parecido posible al entorno de producción.
2.4.3 Test funcionales
Los tests funcionales verifican el correcto funcionamiento de una funcionalidad determinada, dados unos valores de entradas contra una especificación concreta. Los tests funcionales no son conscientes de valores intermedios o efectos colaterales en la ejecución, sólo en el resultado de la ejecución. En desarrollos de aplicaciones web los test funcionales realizan simulaciones de ejecución de controladores (en patrón de diseño Modelo-Vista-Controllador los controladores son las clases responsables de ejecutar la lógica de negocio y asignarla a la vista) y devuelven la respuesta de forma análoga a como lo haría un servidor web como Apache.
2.4.4 Tests de aceptación
En el mundo Agile [44], se considera test de aceptación a los tests que satisfacen las historias de usuarios definidas como especificación del programa o User Stories. Si el test pasa se puede considerar que el software cumple con la especificación y que la historia de usuario se puede considerar como completa. Para realizar este tipo de tests se utilizan simuladores web como puede ser Selenium. Tambien se necesita que la aplicación sea totalmente operativa y funcional, dado que no solo se testea la implementación en PHP, sino todo el sistema en conjunto, incluyendo la interacción con el usuario mediante JavaScript.
Consideramos que estos tipos de tests son los más importantes, aunque hay documentación que habla de otro tipo de tests en función del nivel de testeo o de la especificación de los tests [9].
2.5 SUT y colaborador
En la literatura escrita sobre tests y más en concreto sobre tests unitarios, se utiliza de forma extendida el concepto de sistema bajo tests u objeto bajo tests, con la abreviatura SUT (del inglés, system under test). Nosotros utilizaremos esta misma expresión, utilizando la abreviatura SUT para hacer referencia a la clase que queremos testear. Se le llama colaborador a cualquier otro objeto que afecte al sistema bajo test. El concepto de colaborador tendrá mucho más significado cuando hablemos de test dobles, en el capítulo correspondiente a ello.
2.6 Aserciones o expectativas
En el capítulo 3 ilustraremos esto con ejemplos concretos, pero para dar una mejor visión la pregunta "en qué consiste un test" necesitamos hablar de la unidad de validación sobre la se trabaja cuando se testea. Esto son las aserciones. Un tests no es más que la validación de la respuesta o el comportamiento de nuestro SUT realizado de forma programática.
Cada framework de testeo tiene su propia sintaxis de validación y un conjunto de mecanismos para definir los distintos tipos de validaciones. Normalmente, se realiza una comparación entre la devolución o el estado de la ejecución de nuestro SUT y el valor esperado.
Entre los distintos tipos de aserciones podemos encontrar:
- Igualdad.
- Igualdad estricta (en PHP el operador === es distinto al operador ==, el primero verifica igualdad de valor y de tipo de datos, mientras que el segundo solo igualdad de el valor).
- Relaciones entre Arrays (contiene clave, contiene valor...)
- Una clase tiene un atributo.
- Un objeto es de un tipo.
- Expectativas de excepciones, es decir, la ejecución del SUT en determinadas circunstancias no devolverá ningún valor, sino que lanzará un excepción.
- etc...
2.7 Frameworks de test unitarios
El framework de testeo más conocido en el mundo de desarrollo en PHP es PHPUnit [1]. PHPUnit pertenece a la familia de xUnit. Originalmente, la familia de frameworks xUnit comienza con SUnit en 1989, para Smalltalk, desarrollado por Kent Beck. A dicha familia de frameworks pertenecen otros como JUnit (para Java), RUnit (para R), etc. Dado que PHPUnit es el framework de testeo más extendido, cuando hablemos de testeo unitario nos referiremos a él, siempre que no hagamos referencia a otro.
Además de PHPUnit en el mundo PHP han existido algunas alternativas como Atoum [7] y SimpleTest. Respecto a SimpleTest, en este PFC no haremos referencia dado que el proyecto no ha sido actualizado desde 2012 y entendemos que deja atrás algunas novedades de PHP. Atoum, pese a no tener la popularidad de PHPUnit, es un framework moderno y completo al que haremos alguna mención.
Además de los frameworks clásicos de testeo unitario existen otros orientados ha realizar tests según la especificación del proyecto a desarrollar. Dentro de estos podemos encontrar a Behat y Phpspec. Estos dos nacen inspirados de RSpec, desarrollado para Ruby con una gran popularidad entre los desarrolladores de este lenguaje.
En una posición intermedia a PHPUnit y Behat podemos situar a Codeception. Codeception es otro framework de testeo PHP, desarrollado sobre PHPUnit, pero con funcionalidades específicas para realizar tests funcionales y de aceptación. Dado que es una herramienta que integra las distintas formas de testeo de un proyecto le dedicaremos un capítulo completo.
2.8 Git, Composer y Packagist y estándares PSR
Durante este proyecto haremos menciones constantes a Git y su proyecto web GitHub [10], a Composer [11] y a los distintos estándares que la comunidad de PHP está intentando implantar. Para poner en contexto explicaremos que es cada de estos proyectos:
Git Es un sistema de control de versiones distribuido y de código abierto. Utilizando este proyecto, sus creadores crearon GitHub.com, que es un sitio web especializado en hospedaje de proyectos de software, con versión gratuita para proyectos de código abierto y otra de pago para proyectos privados. En los últimos años se ha convertido en el sitio donde los desarrolladores publican los proyectos de código abierto y la comunidad tiene la oportunidad de contribuir.
Composer y Packagist [12] Composer es un gestor de dependencias para proyectos PHP. Mediante la definición de un fichero en formato json, llamado normalmente composer.json, un proyecto puede utilizar otros proyectos PHP. Packagist.org es sistema centralizado donde se alojan las referencias de estas dependencias, siguen la recomendación de Semantic Versioning. Esta última es una estandarización sobre cómo debe evolucionar el versionado de proyectos de software mediante una numeración de la forma MAJOR.MINOR.PATCH. Composer implementa esta forma estándar permitiendo definir qué versión de un paquete o librería debe ser incorporado en nuestro proyecto.
PHP-FIG [2]: Es un grupo de desarrolladores PHP que están creando estándares dentro de la comunidad, algunos de una gran utilidad como PSR-4 (estándar para autoload de clases), mediante el cual, los proyectos que lo implementan pueden ser reutilizados en otros proyectos con una especificación de namespaces, que puede ser fácilmente definida en Composer.