Testing en PHP
Testing en PHP
Testing en PHP
de contenido
1. Introduction 1.1
2. Agradecimientos 1.2
3. Capítulo 1. Introducción 1.3
4. Capítulo 2. Primer acercamiento a tests 1.4
5. Capítulo 3. Ejemplos de distintos frameworks 1.5
6. Capítulo 4. Aislamiento de tests e interacción con datos 1.6
7. Capítulo 5. Testeando bases de datos 1.7
8. Capítulo 6. Test dobles 1.8
9. Capítulo 7. TDD 1.9
10. Capítulo 8. BDD 1.10
11. Capítulo 9. Codeception: Solución todo en uno 1.11
12. Capítulo 10. Testeando APIs 1.12
13. Capítulo 11. Integración continua 1.13
14. Capítulo 12. Test de proyectos existentes 1.14
15. Capítulo 13. Más Tests 1.15
16. Conclusiones 1.16
17. Bibliografía 1.17
1
Introduction
Introduction
En los últimos años el lenguaje de programación PHP ha experimentado una profesionalización por
parte de la industria, debido principalmente, a que algunas de las empresas más importantes de
internet como Facebook.com están desarrolladas en este lenguaje. Una de las partes del desarrollo en
PHP que más crecimiento ha experimentado ha sido la forma de testear el código, y las distintas
herramientas y metodologías que se han adaptado al ecosistema PHP. Este proyecto trata de presentar
dichas herramientas, las alternativas y los problemas que se encuentran normalmente proyectos PHP.
En nuestra opinión, los acontecimientos que han revolucionado la comunidad PHP han sido un
conjunto de herramientas, buenas prácticas y standards, los cuales han propiciado una comprensión
unánime hacia tener probado un proyecto PHP, como prueba de ello podemos ver al estudiar el código
de cualquier proyecto PHP en http://github.com. La comunidad detrás de PHP-FIG[1] está motivando
a que los desarrollos en PHP sigan estándares que faciliten la modularización y reutilización de dichos
componentes[2]. Bajo esta premisa de poder compartir código es necesario que el comportamiento de
dichos componentes sea predecible y estable, y para ello la comunicad desarrolla testing[3] en las
distintas fases del proyecto, y en distintos capas de abstracción del desarrollo (unitarios, funcionales,
de integración o de aceptación).
Para ello existen herramientas como PHPUnit[4] y phpspec[5] que facilitan la tarea del testo. Los
principales frameworks de la comunidad PHP como pueden ser Symfony, Laravel o Yii tienden ha
implementar este tipo de estrategias, tal y como podemos ver en las separación de componentes
independientes de estas librerías.
Este proyecto trata de documentar, analizar y contrastar las distintas metodologías y herramientas que
han surgido en los últimos años para testear aplicaciones y desarrollos web en PHP, a través de
ejemplos prácticos cuando sea posible.
2
Agradecimientos
Agradecimientos
Este proyecto no hubiera sido posible sin el apoyo que he recibido de muchas personas, a las que se lo
quiero agradecer:
A mi hijo de un año Saúl y a su madre, mi chica, Mari Carmen, por apoyarme en todo
incondicionalmente.
A mi toda familia. En especial a mis padres y hermana, por la paciencia que han tenido en
darme la formación y la educación que he recibido. Tengo que dar gracias por el cariño que
recibí durante todo el tiempo que estudié en Málaga, las facilidades que tuve, los buenos
consejos, y la insistencia porque finalizara mis estudios.
A mis compañeros de estudios. Sin ellos no hubiera podido disfrutar y padecer de todos los
momentos que viví estudiando la ingeniería. A los hermanos Sergio y Javier Jimenez Gonzalez,
a Jaime Gálvez Cordero, Emilio Lucena, Juan Antonio Morales, etc...
A mis amigos y compañeros de viaje universitario. Aquellos que compartían conmigo cada
mañana el autobús o el coche desde Torre del Mar a la escuela en Teatinos. De las
conversaciones que mantuvimos aprendí parte de lo que me enseñó la universidad.
Quiero dar las gracias a todos los profesores, docentes, investigadores y resto de personal
universitario. Sin ellos no sentiría lo que siento ahora por las ciencias de la computación, y no
podría trabajar en algo que me gusta, divierte e ilusiona. En especial quiero agradecer a
Francisco Villatoro. Siempre ha estado disponible para ser mi director de proyecto, además de
realizar una gran labor de divulgación científica.
A mis compañeros de trabajo. En especial a los de las dos empresas a las que he trabajado más
tiempo, Salir.com y Trovit SL. Algunos de los compañeros de trabajo a los que quiero
agradecer especialmente son: Jordi Salvat, Diego Campoy, Sergi de Pablos, Xose Pérez, Raül
Perez, Gorka Lopez, Issel Guberna, Carlos Falo, etc, etc.
3
Capítulo 1. Introducción
Capítulo 1. Introducción
Introducción
1.1 Introducción al proyecto
En los últimos años el lenguaje de programación PHP ha experimentado una evolución por parte de la
industria, debido principalmente, a que algunas de las empresas más importantes de internet como
Facebook.com están desarrolladas en este lenguaje.
Una de las partes del desarrollo en PHP que más crecimiento ha experimentado ha sido la forma de
realizar pruebas automáticas del código y las distintas herramientas y metodologías que se han
adaptado en ecosistema PHP. En este proyecto vamos a trata de presentar dichas herramientas, las
alternativas y los problemas que podemos encontrarnos normalmente al desarrollar y especialmente
testear proyectos PHP.
En nuestra opinión, los acontecimientos que han revolucionado la comunidad PHP han sido la
adopción de un conjunto de herramientas, buenas prácticas y estándares por parte de desarrolladores y
empresas. De este conjunto de herramientas y estándares la comunidad a comprendido los beneficios
de tener testeado un proyecto PHP. Como prueba de ello podemos ver al estudiar el código de
cualquier proyecto PHP alojado en GitHub, que la mayor parte tiene un gran conjunto de pruebas
automáticas en sus respectivos repositorios.
La comunidad detrás de PHP-FIG está motivando a que los desarrollos de PHP sigan estándares que
faciliten la modularización y reutilización componentes. Bajo esta premisa de poder compartir código
es necesario que el comportamiento de dichos componentes sea predecible y estable. Para ello la
comunicad desarrolla tests en las distintas fases del proyecto y a distintos niveles (unitarios,
funcionales, de integración o de aceptación).
En este PFC intentaremos dar visibilidad a las distintas herramientas, centrándonos especialmente a
las que están publicadas bajo licencias de código abierto. También estudiaremos las distintas
metodologías que podemos realizar cuando hablamos de testo automático.
introducción práctica para testeo. Para ello crearemos unos tests sencillos con PHPUnit y
Atoum para una clase de manipulación de cadenas de texto.
Capítulo 5 "Testeando bases de datos". En este capítulo explicaremos los problemas que
existen al testear código con dependencia de una base de datos. Veremos un ejemplo para
documentar la forma en la que se podría testear códigos con este tipo de dependencias.
Capítulo 6 "Test dobles". En este capítulo analizaremos el problema que existe al testear con
dependencias y analizaremos distintas formas de resolver ese problema, así como distintas
alternativas a herramientas que ayudar a este efecto.
Capítulo 7 "TDD". En este capítulo estudiaremos la metodología de desarrollo guiado por los
tests. Estudiaremos los distintos pasos de la metodología a través de un ejemplo y veremos una
herramienta para desarrollar : Phpspec.
Capítulo 8 "BDD" Este capítulo está dedicado a estudiar el desarrollo guiado por
comportamiento (behavior-driven development), así como la principal librería dedicada de PHP
para ello: Behat. Veremos como utilizar Behat con un ejemplo como test de aceptación, aunque
expondremos las bases para usarlo como herramienta de BDD. Al final del capítulo veremos
ligeramente dos alternativas a Behat, Pho y Peridot.
Capítulo 10 "Testeando API". En este capítulo veremos como testear la API pública de un
servicio online, Flickr.com, siguiendo las dos alternativas a las que podemos optar: mockear o
conexión. Veremos los problemas y ventajas al optar una u otra alternativa.
Conclusión. En este capítulo se describen las conclusiones y aprendizajes obtenidos con este
proyecto, así como futuras lineas de trabajo.
Bibliografía. Se describe el material utilizado para realizar este proyecto, tanto libros como
páginas web.
5
Capítulo 2. Primer acercamiento a tests
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.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
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.
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.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.
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
7
Capítulo 2. Primer acercamiento a tests
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].
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.
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...
8
Capítulo 2. Primer acercamiento a tests
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.
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.
9
Capítulo 3. Ejemplos de distintos frameworks
3.2 PHPUnit
Como comentábamos en el capítulo anterior, PHPUnit [1] es el framework más usado para testear
proyectos en PHP y por ello lo vamos a usar en esta para testear XString.
3.2.1 Instalación
En la documentación oficial de PHPUnit podemos encontrar varias formas de ser instalado. Nosotros
vamos a documentar la que creemos que tiene más ventajas y es la instalación de PHPUnit en nuestro
proyecto como dependencia de Composer.
Para instalar PHPUnit con Composer necesitamos añadir la dependencia como podemos ver en el
siguiente texto, que debe estar en el fichero composer.json de la raíz de nuestro proyecto:
{
"require-dev": {
"phpunit/phpunit": "5.0.*"
}
}
composer install
Antes de ver algún ejemplo del código de nuestra clase XString y sus correspondientes tests unitarios
necesitamos documentar cuales son las recomendaciones para organizar un proyecto PHP siguiendo
PSR-4 [2]. PSR-4 es un estándar documentado por PHP-FIG [2] que determina como debe
organizarse el código en namespaces y cómo debe funcionar el autoload de clases.
La estructura básica en la carpeta raíz de un proyecto php debería contener los siguientes ficheros y
carpetas:
* src/
* tests/
* composer.json
10
Capítulo 3. Ejemplos de distintos frameworks
* phpunit.xml
En la carpeta src/ tendremos el código de la aplicación. En la carpeta test/, tendremos el código de los
tests. El fichero phpunit.xml es el fichero que define la configuración que ejecutará los tests de
PHPUnit y es cargado por defecto cuando ejecutamos PHPUnit sin ningún otro parámetro.
En este fichero estamos especificando que, antes de ejcutar los tests, debe incluir el fichero
tests/bootstrap.php, cuyo contenido es:
Necesitamos cargar este fichero para que en la ejecución de los tests, nuestras clases sepan resolver la
localización de los ficheros necesarios mediante el estándar *PSR-4.
Además, en nuestro fichero phpunit.xml estamos creando un Suite de tests, que es la forma de
organizar los tests. También estamos especificando, mediante un filter, que los ficheros que vamos a
incluir acaban con la extensión php.
Las características básicas para realizar tests unitarios utilizando PHPUnit son:
Dada una clase dentro de la carpeta src, crearemos una clase de tests en la carpeta tests con el
mismo nombre que pero con el sufijo "Test" donde desarrollaremos los tests. Por ejemplo, para
testear la clase Clase.php, crearemos la clase ClaseTest.php.
Cada método de testeo que deseemos ejecutar deberá ser método público que comenzará por el
prefijo "test".
A continuación presentamos un fragmento de código de la clase XString. Para evitar que sea muy
extenso hemos incluido solo dos métodos de nuestra clase XString, startWith y endWith.
class XStrings {
protected $str;
11
Capítulo 3. Ejemplos de distintos frameworks
/**
* Construct
*
* @param string $str Str var.
* @param string $encoding Encoding.
* @param boolean $forceEncode Force encoding.
*
* @return void
*/
public function __construct($str, $encoding = 'UTF-8', $forceEncode = true
{
if (mb_detect_encoding($str) != 'UTF-8' && $forceEncode) {
$str = mb_convert_encoding($str, 'UTF-8');
}
$this->str = $str;
}
/**
* Return true, if the string starts with $prefix
*
* @param string $prefix Prefix string.
*
* @return boolean
*/
public function startWith($prefix)
{
return mb_substr($this->str, 0, mb_strlen($prefix)) === $prefix;
}
/**
* Return true, if the string ends with $suffix
*
* @param string $suffix Suffix string.
*
* @return boolean
*/
public function endWith($suffix)
{
return mb_substr($this->str, mb_strlen($this->str) - mb_strlen($suffix
}
}
Y a continuación presentamos la clase de testeo con los dos tests necesarios para verificar que la
implementación de los métodos startWith y endWidth cumplen la especificación deseada. Como
podemos ver, los métodos de testeo se llaman comenzando por el prefijo test como indicábamos
anteriormente.
/**
* Test for startWith method.
*
12
Capítulo 3. Ejemplos de distintos frameworks
* @return void
*/
public function testStartWith()
{
$xstring = new XString('hello world');
$this->assertTrue($xstring->startWith('hello'));
$this->assertFalse($xstring->startWith('world'));
}
/**
* Test for endWith method.
*
* @return void
*/
public function testEndWith()
{
$xstring = new XString('hello world');
$this->assertTrue($xstring->endWith('world'));
$this->assertFalse($xstring->endWith('hello'));
}
}
Para ejecutar los tests debemos lanzar el siguiente comando desde la carpeta de nuestro proyecto:
bin/vendor/phpunit
Debemos ejecutar este comando porque estamos utilizando la versión de PHPUnit instalada para este
proyecto concreto y no le incluimos ningún parámetro al comando porque lo especificamos en el
fichero phpunit.xml, tal y como indicamos anteriormente. Instalar y ejecutar una versión de PHPUnit
especificada en el fichero Composer nos ofrece algunas ventajas. Una vez publicado el proyecto,
cualquier desarrollador puede ejecutar los tests en las condiciones más parecidas en las que se ha
desarrollado, manteniendo incluso la versión de PHPUnit. Además, para sistemas de integración
continua, como veremos más adelante, tendremos también controlada la versión sobre la que se
integra nuestro código.
................................
Si seguimos desarrollando y cometemos un error, como hemos hecho intencionadamente, por el que
los tests unitarios no pasarían, veríamos un resultado similar a este:
.F..............................
13
Capítulo 3. Ejemplos de distintos frameworks
1) Test\XStringTest::testStartWith
Failed asserting that false is true.
/home/jose/workspace/MyThings/TestingBook/Strings/tests/XString/XStringTest.ph
phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:151
phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:103
FAILURES!
Tests: 32, Assertions: 39, Failures: 1.
El ejemplo que hemos tratado para documentar PHPUnit es muy sencillo. En él solo hemos visto dos
tipos de aserciones: assertTrue y assertFalse. En la documentación de PHPUnit podemos ver los
distintos tipos de aserciones: https://phpunit.de/manual/current/en/appendixes.assertions.html.
3.3 Atoum
Otro framework de testeo en PHP es Atoum [7]. En la página oficial de GitHub se define como
simple, moderno e intuitivo para testeo unitario en PHP. Según la especificación Atoum ejecuta cada
test en un proceso separado de PHP, lo cual garantiza aislamiento entre tests. Además, promete una
interfaz fluida y mayor facilidad de crear Stubs (algo que veremos en el capítulo de tests dobles)
gracias a al uso de funciones anónimas y closures.
Otra diferencia notable con respecto a PHPUnit es la sintaxis. Según los autores es más legible y
soporta un estilo de testeo con la expresividad requerida por BDD.
Dado que PHPUnit es el referente en testeo en PHP, pensar en una alternativa y utilizarla en nuestros
proyectos puede sonar arriesgado, pero hay algunos proyectos, como algunos de los proyectos de Hoa
Project (http://hoa-project.net/En/), utilizan Atoum. Uno de estos proyectos es el propio entorno de
testing de la familia Hoa: https://github.com/hoaproject/Test.
Aunque Atoum no tiene la misma difusión y aceptación que PHPUnit, hay un número no despreciable
de proyectos que han decidido basar sus test en este framework. Algunos de estos proyectos son:
Aunque ninguno de estos proyectos son grandes proyectos, son ejemplos de como algunos
desarrolladores han decidido salir de la comodidad de usar PHPUnit y escoger una alternativa. Al
final, lo importante es ejercitar el código que desarrollamos para garantizar su estabilidad en el futuro
y mejor comprensión.
3.3.2 Instalación
Al igual que hicimos con PHPUnit, y aunque hay más formas de instalar Atoum, documentaremos
Composer como método de instalación, con el siguiente código del fichero composer.json:
{
"require-dev": {
14
Capítulo 3. Ejemplos de distintos frameworks
"atoum/atoum": "^2.2"
}
}
composer install
vendor/bin/atoum -v
Reutilziaremos la estructura del proyecto XStrings y añadiremos una carpeta nueva para crear los
tests unitarios: ./test-atoum/unit, donde crearemos el fichero de testing XStringTest.php.
Para ilustrar la sintaxis de Atoum vamos a crear test unitarios para nuestra clase XString, de la misma
forma que lo hicimos con PHPUnit. Viendo el código vemos algunas de las diferencias que hay entre
la sintaxis de Atoum y PHPUnit, por ejemplo las aserciones. PHPUnit mantiene la forma de
"assertXXX", donde XXX es el tipo de aserción. Sin embargo, Atoum tiene otra sintaxis totalmente
distinta. En cada asserción realiza una comprobación de tipo de datos, con los métodos "boolean",
"string", ... y posteriormente llama a un método para comprobar la relación entre el resultado esperado
y el obtenido tras ejercitar el SUT en cuestión. Estas funciones son del tipo: isTrue, isEqualTo, ...
<?php
namespace XString\test\units;
require_once 'src/XString/XString.php';
use \mageekguy\atoum;
use XString\XString as XS;
/**
* Test for startWith method.
*
* @return void
*/
public function testStartWith()
{
$xstring = new XS("hello word");
15
Capítulo 3. Ejemplos de distintos frameworks
$this->boolean($xstring->startWith("hello"))->isTrue();
}
/**
* Test for endWith method.
*
* @return void
*/
public function testEndWith() {
$xstring = new XS("hello world");
$this->boolean($xstring->endWith("world"))->isTrue();
}
vendor/bin/atoum test-atoum/units/XStringTest.php
La forma en la que Atoum nos reporta el resultado de la ejecución de los tests unitarios también es
distinta de PHPUnit. Por defecto, Atoum nos indica la cobertura de tests en cada uno de los métodos
públicos que contiene la clase a testear. En nuestro ejemplo hemos recortado la salida de la ejecución
de los tests, dado que en el momento de esta ejecución teníamos tres métodos testeados y para el resto
de métodos nos reportaba un 0.00% de cobertura, como nos indica en los métodos "delete", "insert" y
"lastPartition".
16
Capítulo 4. Aislamiento de tests e interacción con datos
Supongamos que queremos testear la implementación de una cola LIFO, como en el siguiente
ejemplo. En el ejemplo, nuestra cola está representada por la variable this->queue, que es un array.
En otras situaciones la clase a testear tiene más complicaciones dado que existen otros dependencias.
La situación es más complicada cuando debemos repetir la configuración, es decir, el fixture, para
cada método de testeo.
PHPUnit nos ofrece los métodos setUp y tearDown para ayudarnos en la tarea de la configuración del
los tests. setUp es un método ejecutado antes de cada test, para inicializar las variables y el entorno en
un estado conocido. De la misma forma tearDown es ejecutado después de cada test, pasen o falle,
para limpiar las variables inicializadas. Con estos métodos podemos evitar duplicar código en los
propios tests para configurar el estado conocido.
Si volvemos al ejemplo del capítulo anterior, donde testeabamos la clase XString, podemos introducir
una mejora añadiendo el método setUp he inicializando un atributo de tipo XString para la clase de
test. De esta forma evitaremos tener en cada test el código de inicialización del SUT. Si
17
Capítulo 4. Aislamiento de tests e interacción con datos
refactorizamos el ejemplo del apartado anterior con el método setUp y tearDown podría quedar de la
siguiente forma:
/**
* Test for startWith method.
*
* @return void
*/
public function testStartWith()
{
$this->assertTrue($this->xstring->startWith('hello'));
$this->assertFalse($this->xstring->startWith('world'));
}
/**
* Test for endWith method.
*
* @return void
*/
public function testEndWith()
{
$this->assertTrue($this->xstring->endWith('world'));
$this->assertFalse($this->xstring->endWith('hello'));
}
}
18
Capítulo 4. Aislamiento de tests e interacción con datos
Una anotación es una forma especial de añadir meta información a los tests que puede ser añadido en
el código fuente con el fin extender el comportamiento de dichos tests.
En la documentación oficial de PHPUnit [1] podemos ver todas las anotaciones. Algunas de estas
anotaciones las comentaremos en las siguientes secciones, como son @backupGlobals o
@dataProvider.
Aunque no sea propio de este capítulo debemos destacar otro tipo de anotaciones, por su importancia,
como son las que determinan que el test con dicha anotación espera recibir una excepción por parte
del SUT. Un ejemplo del formato de este tipo de test sería:
El test testException pasará si el SUT que es ejecutando lanza una excepción del tipo
InvalidArgumentException, en caso el test devolverá un error.
Generalmente testear estados globales es difícil dado que no se puede predecir la situación en la que
se encuentra en la ejecución de un test concreto.
Este tipo de situaciones son las propias a las que producen el uso de clases que implementan el patrón
de diseño Singleton o variables globales.
En PHP, las variables globales son aquellas propias del entorno y son: GLOBALS, _ENV, _POST,
19
Capítulo 4. Aislamiento de tests e interacción con datos
_GET, _COOKIE, _SERVER, _FILES, _REQUEST. El riesgo de modificar el valor de estas variables
es que, al ser compartidas, el cambio de un valor en un test puede romper otro.
Para solventar esta situación PHPUnit ofrece la directiva @backupGlobals. Con esta anotación el
propio framework guarda el estado de las variables globales antes de la ejecución de un test y lo
restaura después.
La anotación dataProvider especifica a un test una función que devolverá los parámetros de entradas
del test. En el siguiente ejemplo podemos ver como la función additionProvider devuelve los datos de
entrada del test testAdd, de la siguiente forma:
En esta sección veremos tres herramientas: Faker [13], Alice [14] y Samsui [15]. Faker es un
generador de datos, Alice es un conector entre datos y objetos existentes y Samsui es una solución
intermedia, aunque más inmadura que la combinación de las otras dos alternativas.
4.2.1 Faker
Con este componente podemos generar fixtures, ya sean persistidos o generados de forma dinámica.
Faker genera fixtures especificados por distintos proveedores y distintos formatos de forma aleatoria.
Con esta herramienta, crear fixtures para entidades resulta fácil. La implementación de Faker permite
configurar el idioma y el valor de los formatos en función a dicho idioma. Por ejemplo, podríamos
crear un conjunto de elementos utilizando el proveedor de dirección en formato para la localización
de Estados Unidos. Con esta configuración, Faker se encargaría de producir una serie de direcciones
20
Capítulo 4. Aislamiento de tests e interacción con datos
Ilustremos el uso de este componenete con un ejemplo. Supongamos que estamos desarrollando un
sistema de opiniones de usuarios. Para dicho sistema necesitaremos la clase User y la clase Review.
Un usuario tiene dos parámetros de entrada, username y email, y los métodos públicos addReview y
countReviews.
La clase Review se construye con un title y description, como título y descripción de la opinión. En el
ejemplo podemos ver como utilizamos el componente Faker tanto para crear usuarios como
comentarios.
class User {
protected $username;
protected $email;
class Review {
protected $title;
protected $description;
$faker->addProvider(new Lorem($faker));
for($i=0;$i<=10;$i++) {
$this->user->addReview(new Review($faker->sentence, $faker->paragr
}
$this->assertEquals(10, $this->user->countReviews());
}
}
Como podemos ver en el código del método testAddReview, generar una lista con 10 usuarios con
distinta información es trivial si utilizamos Faker. De esta forma nos podemos ahorrar crear datos y
mantenerlos en ficheros en algún formato como csv y json y mantenerlos en un futuro. Dado que el
ejemplo es muy sencillo y apenas tiene de lógica, no estamos viendo otro tipo de ventajas que nos
puede aportar estas herramientas. Por ejemplo, si necesitaramos validar algún tipo de dato, como e-
mail, url, etc. tendríamos un conjunto de datos por lo cual nuestra implementación podría fallar.
En la documentación de Faker [13] podemos ver todas las posibilidades que ofrece a la hora de
generar datos.
4.2.2 Alice
Cuando trabajos con fixtures, normalmente necesitamos algo más de un conjunto de datos. Nuestro
SUT suele ser dependiente de otras partes del sistemas, como pudimos ver en el ejemplo del apartado
dedicado a Faker. La solución por la que optamos en ese ejemplo fue la de crear objetos "Review"
dentro del método de testeo, pero este trabajo también se puede automatizar.
Para esa tarea existe el componente Alice. Alice es una herramienta para crear objetos concretos a
partir de ficheros de configuración (PHP o Yaml). Además, Alice tiene como dependencia Faker, y
puede ser utilizado para generar de forma dinámica fixtures aleatorios.
Podríamos refactorizar el método testAddReview mediante una configuración, en este caso en formato
Yaml, contenido en el fichero "fixture/review.yml":
Review:
review{1..10}:
title: <sentence()>
description: <paragraph()>
review{1..10} de diez objetos de la clase Review, con atributos generados por Faker. El instanciación
de los objetos con la configuración propia es realizada por el loader de Alice, por lo que simplifica la
configuración de nuestro test. Este ejemplo es poco ilustrativo al ser sencillo, pero nos da una idea de
como funciona este tipo de herramientas y cómo se puede utilizar en ejemplos más complejos.
La combinación de estas dos herramientas nos dan una gran flexibilidad a la hora de generar datos
que puedan ayudar a crear un conjunto de tests para que nuestra aplicación sea robusta.
4.2.3 Samsui
Samsui es una librería diseñada para crear objetos PHP con el objeto de testear aplicaciones. Está
inspirada en las librerías Rosie (de JavaScript) y factory_girl (de Ruby).
Es una solución a los problemas que soluciona Faker y Alice con una única herramienta. Pese a que es
una solución completa, está menos desarrollada que Faker y en el momento de la edición de este PFC
no está adaptada para crear fixtures en distintos idiomas y el conjunto de generadores (tipo de datos a
crear) no es mejor que el de las otras alternativas. Con respecto a la generación de objetos, tampoco
tiene la capacidad de Alice de crear objetos de tipos concretos.
use Samsui\Factory;
use Samsui\Generator\Generator;
Como vemos, la generación de objetos de tipo "person" con Samsui es sencilla, al especificar el valor
de cada atributo del objeto mediante la clase Generator.
23
Capítulo 4. Aislamiento de tests e interacción con datos
Como comparativa entre las herramientas vistas para la generación automática de fixtures podemos
concretar que la mejor opción, al menos entre las estudiadas, es la combinación Faker y Alice, aunque
Samsui promete en un futuro ser una alternativa válida. Para proyectos con ambitos internacionales
tener una herramienta que nos ayude a crear un conjunto de datos para testing inicial en idiomas que
desconocemos como Faker puede resultar de gran ayuda.
24
Capítulo 5. Testeando bases de datos
Test unitario, utilizando test dobles. Test tipo de tests lo explicaremos en el capítulo 6, dedicado
a tests dobles, aunque veremos otras situaciones como en el capítulo 10, dedicado a testear API.
Resumiendo: consiste en falsear objetos que simulan el comportamiento de la base de datos,
por lo que testearíamos la lógica de más alto nivel de las clases que afecten a dicha base de
datos. No podríamos considerar un tests de base de datos realmente, aunque en muchas
situaciones este tipo de tests son suficientes.
Integración, con fixture de datos concretos. Este tipo de tests acceden realmente a base de datos,
pero el estado de la base de datos es conocido, dado que antes de la ejecución del conjunto de
tests se realizan tareas para crear y rellenar la base de datos.
Test de aceptación. Estos tests simulan la aplicación en su conjunto. Para estos tests en entornos
web la aplicación será ejecutada por completo, ejecutando un navegador y simulando el
comportamiento de un usuario interactuando con algún mecanismo de simulación de
navegadores web. Aunque el objetivo de estos tests no sea específicamente verificar la
interacción entre la aplicación y la base de datos, como efecto de realizar pruebas sobre todo el
sistema, la base de datos también quedará ejercitada y testeada.
La funcionalidad que se quiere testear ejecuta una compleja consulta con entre tablas (join).
La lógica de negocio ejecuta SELECT, INSERT o DELETE.
La inicialización de los datos no es sencilla, requiere mecanismos de inserción previos a la
ejecución de los tests y de limpieza de los datos una vez ejercitados los tests.
La gestión del esquemas de base de datos y tablas, desde el punto de vista de mantenibilidad.
Con esto queremos decir que para mantener funcionales los tests de bases de datos necesitamos
actualizar la base de datos de nuestro SUT con relación a la base de datos de producción.
Los tests que ejecutan sentencias en base de datos deben ser cortos por las siguientes razones:
Configurar el fixture.
Ejercitar el sistema bajo test.
Verificar los resultados.
Limpiar el fixture.
Estos pasos no son exactamente los mismos que debemos ejecutar cuando testeamos bajo
dependencia de la base de datos. Los pasos que debemos considerar son:
Limpiar la base de datos: Para tener un entorno bajo un sistema de control la base de datos debe
de estar en un estado conocido. La forma de comenzar de forma segura es limpiar la base de
datos.
Configurar el fixture: En un test sin dependencia de base de datos, configurar el fixture, como
vimos en el capítulo 4, consiste en la inicialización de los objetos que interactuarán con nuestro
SUT. En un test con bases de datos necesitaremos crear el contenido necesario realizando las
operaciones de inserción sean apropiadas para conocer el estado inicial de la base de datos.
Ejecutar y verificar los resultados, como en cualquier tests.
Ejecutar el proceso de tearDown, para dejar la base de datos en un estado conocido para la
ejecución de los siguientes tests.
Dependiendo de si lo que deseamos realizar son test de integración o funcionales, mantener la base de
datos de test es más complejo. Para los tests de integración es necesario mantener la estructura de la
base de datos, procedimientos, triggers y otras configuraciones. Para realizar test de aceptación es
necesario tener, al menos, un subconjunto de los datos. Dado que los tests funcionales son, al fin y al
cabo, simulaciones de usuarios reales.
En algunos proyectos esto se resuelve manteniendo una copia parcial de los datos reales. Esta práctica
puede tener algún incumplimiento con leyes sobre privacidad de datos, por lo que una mejor solución
sería crear una simulación de la base de datos, lo más parecida posible a producción. En el capítulo 4
vimos herramientas, como Faker [13], que nos pueden ayudar a generar el conjunto de datos
necesarios para simular un entorno funcional completo.
Por otra parte, getDataSet es un método para crear el fixture inicial en base de datos sobre el que
26
Capítulo 5. Testeando bases de datos
realizaremos los tests. En la propia documentación de PHPUnit recomiendan como buena práctica
crear una implementación de PHPUnit_Extensions_Database_TestCase que sea compartida para
todas las clases de testeo de base de datos, con el objetivo tener en un único lugar la conexión y datos.
Esta última recomendación mantiene el principio DRY (del inglés, don't repeat yourself) cosa que en
software es una buena práctica.
Para preparar el fixture, PHPUnit nos da la posibilidad de importar distintos tipos de ficheros:
PHPUnit ofrece distintas funciones para procesar el fixture, como sustituciones (para reemplazar
cambios de valores de campos), filtros (para seleccionar campos o filas en caso de fixtures grandes),
composiciones (para agregar varios fixture en uno) y aserciones propias de bases de datos
(assertTablesEqual y assertDataSetsEqual).
Los tests que realizaremos son de integración y no utilizaremos las herramientas propias de PHPUnit.
Por el contrario, utilizaremos una simulación simplificada de DbUnit, incluida en PHPUnit [1],
aunque el concepto es el mismo.
Dado que utilizaremos PDO con la forma FETCH_CLASS, es decir, asociando a una clase el
resultado de una consulta necesitaremos las clases User y Photo:
27
Capítulo 5. Testeando bases de datos
class User {
public $id;
public $username;
public $email;
public $photos = Array();
public function getPhotos() {
return $this->photos;
}
public function addPhoto(Photo $photo) {
$this->photos[] = $photo;
}
public function addPhotos(array $photos) {
$this->photos = array_merge($this->photos, $photos);
}
}
class Photo {
public $id;
public $user_id;
public $url_photo;
}
Para gestionar el acceso a base de datos hemos creado las clases UserRepository y PhotoRepository,
de forma que sean estas clases las responsables de ejecutar las consultas.
class UserRepository
{
protected $db;
if (count($user->getPhotos()>0)) {
$photoRepository = new PhotoRepository($this->db);
foreach($user->getPhotos() as $photo) {
$photoRepository->storePhoto($photo, $user);
}
28
Capítulo 5. Testeando bases de datos
return $user;
}
return $user;
}
}
class PhotoRepository {
protected $db;
29
Capítulo 5. Testeando bases de datos
}
$photo->id = $photoId;
return $photo;
}
}
Para la ejecución de los tests unitarios hemos decidido utilizar un conjunto de datos generado de
forma dinámica. Para ello, y como vimos en el capítulo 4, utilizado Faker [13] y Alice [14], con el
siguiente fichero YAML:
User:
user{1..10}:
username: <username()>
email: <email()>
Photo:
photo{1..3}:
url_photo: <imageUrl()>
$user = $this->userRepository->storeUser($user);
$this->assertObjectHasAttribute('id', $user);
}
$user = $this->data["user1"];
$this->assertNull($this->data["photo1"]->id);
$this->assertNull($this->data["photo2"]->id);
$this->assertNull($this->data["photo3"]->id);
$user->addPhoto($this->data["photo1"]);
$user->addPhoto($this->data["photo2"]);
$user->addPhoto($this->data["photo3"]);
$user = $this->userRepository->storeUser($this->data['user1']);
$photos = $user->getPhotos();
foreach($photos as $photo) {
$this->assertGreaterThan(0, $photo->id);
}
}
El motivo por el que hemos decidido no seguir la forma normal de testar con dependencias con bases
de datos es porque la documentación extendida y algunas funcionalidades fueron introducidas en
PHPUnit 3.5. Antes de dicha versión se podían testear código dependiente de base de datos, pero
requería algunos procesos. Por ejemplo, para la carga del fixture inicial se puede ejecutar un el
31
Capítulo 5. Testeando bases de datos
comando "mysql load data" en el método setUp de cada test, y el truncate tal y como hacemos en
nuestro ejemplo.
Las versiones posteriores PHPUnit 3.4 facilitan estas tareas, pero los conceptos de mantener la base
de datos en un estado bajo control y los mecanismos para ello, siguen siendo análogos.
En este ejemplo hemos decidido realizar un test de integración y ejercitar el código que accede
directamente a la base de datos. Testear una pieza de código con dependencias de otros sistemas como
base de datos se puede realizar de otra forma, utilizando tests dobles, como mencionábamos al
principio del capítulo. Dado que este capítulo se centraba especialmente en tests de base de datos
hemos decido no hacer test con tests dobles y ejercitar las consultas directamente.
32
Capítulo 6. Test dobles
De una forma gráfica, si queremos testear el método doSomething del siguiente fragmento de código:
class ClassToTest {
Viendo este sencillo ejemplo podemos entender que testear el comportamiento aislado del método
doSomething, no es posible al tener la dependencia del comportamiento del método
doSomethingNotToTest de la clase ClassNoToTest. Para solventar este tipo de situaciones se utilizan
distintos formas de simular o controlar el comportamiento de la clase ClassNoToTest. Para tener una
cobertura de test completa hay que testear cada clase de forma aislada, por lo tanto necesitaremos
crear los mecanismos necesarios para la clase ClassNoToTest de nuestro ejemplo.
Aunque en este mismo capítulo explicaremos con más detalle Prophecy, en esta sección utilizaremos
esta librería de PHP dedicada a crear este tipo de objetos.
6.2.1 Dummy
33
Capítulo 6. Test dobles
Dummy son un tipo de objetos utilizados en test dobles que no tienen comportamiento alguno, dado
que no queremos tener efectos laterales. Son usados para cumplir las restricciones de argumentos de
parámetros tipados. Un ejemplo de uso de dummy sería el siguiente, utilizando Prophecy:
El método prophesize de PHPUnit 4.6 nos devuelve un objeto de tipo Prophet sin ningún tipo de
configuración, que es lo que nos interesa en este tipo de testeo. Como veremos cuando hablemos de
Prophecy, el objeto reveal devuelve la instancia de DummyClass, que es necesaria para instanciar un
objeto de la clase MyClass. El código del constructor de la clase MyClass, por el que es necesario
pasar un objeto del tipo DummyClass sería el siguiente:
class MyClass {
protected $obj;
6.2.2 Fake
En determinadas circunstancias, necesitamos controlar las llamadas a los objetos con dependencias
dentro de nuestro SUT. Para ello, es necesario otro tipo de objetos, los cuales, además de especificar
la clase, se le especifica una configuración determinada para llamadas a métodos concretos. Para este
tipo de situaciones tampoco tenemos dependencia con el resultado de los métodos declarados, pero sí
necesitamos tenerlos especificados dado que el SUT hará llamada a dichos métodos. Un ejemplo de
este tipo de tests objetos sería:
En este ejemplo también hemos usado Prophecy. La instancia fakeLogger es un objeto que en la
aplicación en sí no devuelve resultado a cada llamada a los métodos log o info, pero sabemos que
nuestra clase Controller ejecuta en determinadas circunstancias. Al querer romper la dependencia con
la clase Logger real, hemos creado un objeto Fake el cual no hará nada, pero permitirá que la
ejecución del SUT (en este caso Controller) sea posible.
6.2.3 Stubs
La práctica de reemplazar un objeto con un test doble que devuelve una salida configurada se llama
stubbing. Introduciendo en el SUT este objeto con usa salida predeterminada, tendremos control sobre
la pieza de código que producía la indirección y por lo tanto podremos considerar que estamos
34
Capítulo 6. Test dobles
class Controller {
protected $response;
return true;
}
return false;
}
}
Podríamos crear un test en el que rompanmos la dependencia con el objeto response inicializando la
clase Controller con un objeto stub de la siguiente forma:
Como se puede ver en la implementación de nuestra clase de ejemplo Controller, la dependencia del
método "getHttpStatus" del objeto this->response la configuramos y dejamos bajo control con nuestro
objeto stub, al cual le hemos indicado que devuelva el código de estado 200, con la configuración
willReturn(200). Así el objeto colaborador SolrResponse queda en una situación conocida, lo que nos
permitirá predecir el comportamiento del método doController.
6.2.4 Mocks
La práctica de reemplazar un objeto con un test doble que verifica expectativas, por ejemplo, que un
método concreto del objeto colaborador ha sido llamado, se llama mock.
Utilizar mocks tiene sentido cuando hay una dependencia conocida con el comportamiento de los
elementos que crean la indirección. En la documentación de PHPUnit [1] podemos ver un ejemplo
ilustrativo al testear el patrón de diseños observador. El código de las clases Subject y Observer sería
el siguiente:
class Subject
35
Capítulo 6. Test dobles
{
protected $observers = array();
protected $name;
// Other methods.
}
class Observer
{
public function update($argument) {
// Do something.
}
// Other methods.
}
36
Capítulo 6. Test dobles
$subject->doSomething();
}
}
El test del ejemplo anterior pasará dado que hemos definido que el método update será llamado una
vez y es exactamente lo que ocurrirá si seguimos la traza de ejecución de la llamada doSomething()
del objeto subject.
Si en una refactorización futura de la clase Subject eliminamos la llamada update de la clase Observer
el test anterior fallaría, dado que no se cumpliría la expectativa de llamar una vez el método *update".
Aunque en todos los frameworks de tests utilizamos la misma nomenclatura para construir stubs y
mocks, los dos tipos de objetos son muy distintos. Por un lado, los stubs son objetos que se controlan
la devolución de los objetos colaboradores de nuestro SUT, mientras que los mocks testean el
comportamiento de dichos colaboradores. Por otro lado, los dos tipos de objetos se utilizan con una
filosofía totalmente distinta de diseño y testeo. Esto puede llevar a debate sobre cuando es necesario
utilizar mocks y stubs, y cuando podemos utilizar instancias de las clases colaboradoras dentro del
SUT concreto. Este debate, con muchos más matices y detalles lo expone Martin Fowler en su
artículo "Mocks Aren't Stubs" [19] . Es un debate que, en nuestra experiencia profesional, se repite
constantemente.
De la misma forma que existe el debate sobre testear todo con mocks, o no usar nada, un punto
intermedio parece razonable. Además de que el uso de estos frameworks de testeo ayudan a romper
dependencias entre el SUT y los colaboradores que interactúan con sistemas externos, como llamadas
a API por el protocolo HTTP, llamadas a disco, etc...
37
Capítulo 6. Test dobles
Pese a ser un indicador de mala práctica, este problema se puede resolver mediante mocks parciales.
Estos objetos, como comentábamos anteriormente, heredan directamente de la clase mockeada, pero
lo que el comportamiento de todos los métodos públicos pueden ser configurados después de la
creación. Vamos a explicar este concepto con un ejemplo. Supongamos que tenemos la clase
DoSearch que realiza una búsqueda en alguna página en internet, cuyo código sería:
class DoSearch {
protected $searchTerm;
return file_get_contents($url);
}
}
Dado que queremos hacer el test unitario de esta clase, nos encontramos con el problema de que
testear el método fetch crea una dependencia exterior no deseada. Utilizando Mockery podemos
resolver este problema mediante la creación de un mock parcial. Con Mockery lo podemos hacer de
dos formas:
Este ejemplo creará un objeto que hereda directamente de DoSearch solo con el método "fetch" con el
comportamiento establecido.
$mock = Mockery::mock('DoSearch')->makePartial();
38
Capítulo 6. Test dobles
$mock = Mockery::mock('TwitterSearch')->makePartial();
$mock->shouldReceive('fetch')->once()->andReturn('foo');
$mock->fetch(); // devuelve foo.
Para resolver el problema de mockear el método fetch en los tests de la clase DoSearch sólo bastaría
con crear un objeto mockeado parcialmente en el método setUp de nuestro tests, para PHPUnit.
Para ver la expresividad y las principales diferencias que existen entre estas librerías hemos creado
una extensión de Monolog [21] para poder almacenar logs en el sistema de indexación Apache Solr.
Este ejemplo es solo ilustrativo y no está pensado para utilizarlo en un entorno en producción.
Dado que de forma original Monolog no soporta Apache Solr, nos parece un ejercicio interesante
implementar la lógica necesaria para enviar logs a este sistema de búsquedas y testearlo utilizando test
dobles.
En las siguientes secciones mostraremos como quedaría un tests unitario para las clases SolrHandler
y SolrFormatter de nuestra extensión de Monolog. El código de las clases a testear sería:
namespace MonologExtended\Handler;
use MonologExtended\Formatter\SolrFormatter;
use Monolog\Logger;
use Solarium\Client;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Formatter\FormatterInterface;
use Solarium\QueryType\Update\Query\Document\DocumentInterface;
use Solarium\Core\Query\QueryInterface;
use Solarium\QueryType\Update\Query\Document\Document;
/**
* SolrHandler.
39
Capítulo 6. Test dobles
*/
class SolrHandler extends AbstractProcessingHandler
{
/**
* Client.
*
* @var client.
*/
protected $client;
/**
* Options.
*
* @var options.
*/
protected $options;
/**
* Constructor
*
* @param array $options Options for configure the Solr connection.
* @param integer $level Level of logging.
* @param boolean $bubble Bubble option.
*
* @return void
*/
public function __construct(array $options, $level = Logger::DEBUG, $bubbl
{
$this->client = new \Solarium\Client($options);
parent::__construct($level, $bubble);
$this->options = $options;
}
/**
* Setter for Client property.
*
* @param Client $client Client to make the request.
*
* @return void
*/
public function setClient(Client $client)
{
$this->client = $client;
}
/**
* Set formmater.
*
* @param FormatterInterface $formatter Formater object.
*
* @return FormatterInterface
40
Capítulo 6. Test dobles
/**
* Getter options.
*
* @return array
*/
public function getOptions()
{
return $this->options;
}
/**
* Getter for default formatter.
*
* @return FormatterInterface
*/
protected function getDefaultFormatter()
{
return new SolrFormatter($this->client);
}
/**
* Handle a collection of records.
*
* @param array $records Array of records.
*
* @return void
*/
public function handleBatch(array $records)
{
$documents = $this->getFormatter()->formatBatch($records);
$this->bulkSend($documents);
}
/**
* Write record
*
* @param array $record Record.
*
* @return void
*/
protected function write(array $record)
41
Capítulo 6. Test dobles
{
$this->bulkSend(array($record));
}
/**
* Send a bulk of documents to Solr.
*
* @param array $documents Array of documents.
*
* @return void
* @throws \RuntimeException Something wrong happen sending data to solr.
*/
protected function bulkSend(array $documents)
{
try {
$update = $this->client->createUpdate();
foreach ($documents as $document) {
$update->addDocument($document['formatted']);
$update->addCommit();
}
$result = $this->client->update($update);
} catch (\Exception $e) {
if (empty($this->options['ignore_errors'])) {
throw new \RuntimeException('Error sending messages to Solr'
}
}
}
}
namespace MonologExtended\Formatter;
use Monolog\Formatter\NormalizerFormatter;
/**
* SolrFormatter. Extension of Monolog.
*/
class SolrFormatter extends NormalizerFormatter
{
/**
* Index for sending the messages.
*
* @var index
*/
protected $index;
/**
* Type.
*
* @var type
*/
protected $type;
42
Capítulo 6. Test dobles
/**
* Client.
*
* @var client
*/
protected $client;
/**
* Constructor.
*
* @param mixed $client Client.
*/
public function __construct($client)
{
parent::__construct(\DateTime::ISO8601);
$this->client = $client;
}
/**
* format function
*
* @param array $record Record to be formatted.
*
* @return Document
*/
public function format(array $record)
{
$record = parent::format($record);
return $this->getDocument($record);
}
/**
* Format a collection of records
*
* @param array $records Records to be formatted.
*
* @return array
*/
public function formatBatch(array $records)
{
$docs = array();
foreach ($records as $key => $record) {
$records[$key]['formatted'] = $this->getDocument($record);
}
return $records;
}
/**
* Getter index.
*
43
Capítulo 6. Test dobles
* @return string
*/
public function getIndex()
{
return $this->index;
}
/**
* Getter type.
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Get document function
*
* @param mixed $record Record to make a SolrDocument.
*
* @return Document
* @throws \RuntimeException The SolrDocument couldn't be created.
**/
protected function getDocument($record)
{
try {
$update = $this->client->createUpdate();
$doc = $update->createDocument();
$doc->id = uniqid();
$doc->description = $record['message'];
$doc->title = $record['level_name'];
return $doc;
} catch (\Exception $e) {
throw new \RuntimeException("Can't create an Solr document"
}
}
}
Dado que este ejemplo lo vamos a basar en PHPUnit, el método setUp que compartirán todos los tests
con un mensaje de error de ejemplo para registrar en Apache Solr sería:
/**
* setUp method.
*
* @return void
*/
public function setUp()
{
44
Capítulo 6. Test dobles
$this->msg = array(
'level' => Logger::ERROR,
'level_name' => 'ERROR',
'channel' => 'meh',
'context' => ['foo' => 7, 'bar',
'class' => new \stdClass()],
'datetime' => new \DateTime('@0'),
'extra' => array(),
'message' => 'Logging an error',
);
}
6.5.1 PHPUnit
PHPUnit contiene herramientas suficientes para generar los distintos tipos de objetos colaboradores
(Dummy, Fake, Stub y Mocks), pero con algunas desventajas respecto a las otras librerías. Entra las
ventajas a destacar es que es la más flexible, dado que se puede crear objetos y métodos no
implementados en las clases a testear. Dentro de las desventajas habría que destacar la sintaxis. Como
veremos en el siguiente fragmento de código, en el que testeamos las distintas formas de enviar un log
con nuestra extensión de Monolog, las funciones para gestionar mocks de PHPUnit son incómodas de
escribir:
/**
* test creating a Log using Mocks of PHPUnit
*
* @return void
*/
public function testAddLogWithPHPUnitMock()
{
$client = $this->getMockBuilder('Solarium\Client')
->setMethods(array('addDocuments', 'createUpdate', 'update'
->disableOriginalConstructor()
->getMock();
$updateMock = $this->getMockBuilder("Solarium\QueryType\Update\Query\Q
->setMethods(array('addDocument', 'addCommit', 'createDocument'
->disableOriginalConstructor()
->getMock();
$updateMock->expects($this->any())
->method('addDocument');
$className = "Solarium\QueryType\Update\Query\Document\Document"
$documentMock = $this->getMockBuilder($className)
->disableOriginalConstructor()
->getMock();
$updateMock->expects($this->any())
->method('createDocument')
->willReturn($documentMock);
45
Capítulo 6. Test dobles
$client->expects($this->any())
->method('createUpdate')
->willReturn($updateMock);
En PHPUnit, una vez creado un objeto colaborador es necesario indicarle los métodos que van a ser
sobreescritos mediante el método setMethods. Además, es necesario indicar al framework que no
utilice el constructor propio de la clase colaboradora.
6.5.2 Prophecy
Desde la versión 4.5 de PHPUnit, Prophecy [6] está incluido por defecto. Prophecy es un flexible y
completo framework para la creación y gestión de mocks en PHP. Una de las características que más
llama la atención de Prophecy es la forma de expresar los tests. Como se puede ver en la tabla 6.1,
"Diferencias entre distintos framewors de mocks", Prophecy es estricto en el sentido de que no se
puede falsear nada que no esté implementado en la clase a configurar. Esto se debe a que sigue la
filosofía de RSpec (framework de testeo en el lenguaje de programación Ruby). Otra de las ventajas
es la posibilidad de uso de espías.
Los espías son manipulaciones de tests dobles que permiten almacenar los comportamientos de los
objetos colaboradores y ser consultados sin necesidad de ser especificados en la creación de los tests.
Prophecy graba todas las llamadas de los objetos falseados, por lo cual realizar comprobaciones tras
la ejecución del objeto bajo test, es relativamente sencillo.
$em = $prophet->prophesize('Doctrine\ORM\EntityManager');
$controller->createUser($em->reveal());
$em->flush()->shouldHaveBeenCalled();
El tests que verificaría el funcionamiento de nuestra extensión de Monolog podría tener la siguiente
forma:
/**
* test creating a Log using Mocks of Prophecy
*
* @return void
*/
public function testAddLogWithProphecy()
{
$documentProphecy = $this->prophesize(Document::class);
$updateProphecy = $this->prophesize(Query::class);
$updateProphecy->addDocument(Argument::any())->shouldBeCalled();
$updateProphecy->addCommit()->shouldBeCalled();
$updateProphecy->createDocument()->shouldBeCalled();
$updateProphecy->createDocument()->willReturn($documentProphecy->revea
46
Capítulo 6. Test dobles
$clientProphecy = $this->prophesize("Solarium\Client");
$clientProphecy->createUpdate()->willReturn($updateProphecy->reveal())
$clientProphecy->createUpdate()->shouldBeCalled();
$clientProphecy->update($updateProphecy)->shouldBeCalled();
Como podemos ver, la creación de los objetos colaboradores se realizan con el método prophesize, sin
necesidad de especificarle ningún método ni otro tipo de configuraciones. El comportamiento viene
definido por las expectativas que creamos con la llamada a cada método existente en la clase. Por
ejemplo, la llamada al método shouldBeCalled indica que el método precedente debería ejecutarse
para que ese test sea considerado como válido. De la misma forma, willReturn especificará la salida
que tendrá el método que le precede.
6.5.3 Mockery
Otra alternativa para trabajar con tests dobles es Mockery. Dentro de la comunidad PHP es una opción
bastante utilizada y extendida, pese a que el hecho de que Prophecy esté incluido dentro de PHPUnit,
como indicábamos anteriormente.
Siguiendo con el mismo ejemplo de nuestra extensión de Monolog, el código que validaría nuestra el
envío de mensajes a Apache Solr por parte de Monolog podría ser el siguiente:
/**
* test creating a Log using Mocks of Mockery
*
* @return void
*/
public function testAddLogWithMockery()
{
$mockStrDocument = "Solarium\QueryType\Update\Query\Document\Document[
$documentMockery = \Mockery::mock($mockStr);
$methods = "addDocument,addCommit,createDocument";
$mockStrUpdate = "Solarium\QueryType\Update\Query\Query[$methods]"
$updateMockery = \Mockery::mock($mockStrUpdate );
$updateMockery->shouldReceive('addDocument')->atLeast(1);
$updateMockery->shouldReceive('addCommit')->atLeast(1);
$updateMockery->shouldReceive('createDocument')
->atLeast(1)
->andReturn($documentMockery);
47
Capítulo 6. Test dobles
$clientMockery = \Mockery::mock(Client::class);
$clientMockery->shouldReceive('createUpdate')->andReturn($updateMocker
$clientMockery->shouldReceive('update')->with($updateMockery)->atLeast
Mockery, de forma similar a PHPUnit necesita que le indiquen los métodos a configurar como vemos
en la creación del mock asignado a la variable updateMockery, por ejemplo.
6.5.4 Phake
Phake es otro framework que trata de proveer mocks y stubs para la ejecución de test dobles. Está
inspirado en Mockito (framework de test para Java). En la documentación oficial matiza que la
ventaja de Phake frente a PHPUnit, PHPMock y SimpleTest es que de forma nativa implementa el
concepto de espía. La verdad es que esta no es una ventaja significativa, dado que tanto Prophecy
como Mockery también lo implementan y en PHPUnit se puede llegar a conseguir.
/**
* test creating a Log using Mocks of Phake
*
* @return void
*/
public function testAddLogWithPhake() {
$document = \Phake::mock(Document::class);
$update = \Phake::mock(Query::class);
\Phake::when($update)->createDocument()->thenReturn($document);
$update->addDocument($document);
$update->addCommit();
\Phake::verify($update)->addDocument($document);
\Phake::verify($update)->addCommit();
$client = \Phake::mock(Client::class);
\Phake::when($client)->createUpdate()->thenReturn($update);
$client->update($update);
48
Capítulo 6. Test dobles
6.5.5 AspectMock
AspectMock es otra herramienta para generar mocks en PHP, desarrollada por la comunidad detrás de
Codeception.
namespace demo;
test::func('demo', 'time', 'now');
$this->assertEquals('now', time());
Testear funcionalidades que dependan de la función time suele tener una complejidad adicional, dado
el valor devuelto por esta función siempre es distinto. Aunque el ejemplo aprovecha la forma en la
que funcionan los namespaces en PHP, con este framework de test dobles tenemos la posibilidad de
alterar en tiempo de ejecución el comportamiento de la función time.
Para resolver las dependencias con el sistema de fichero hay librerías como vfsStream [22].
Algunas funcionalidades que necesitamos cumplir cuando desarrollamos software tiene dependencias
de otras partes del sistema, como del sistema de ficheros. Esto genera otro punto en el que testear un
fragmento de código aislado es complicado por dicha dependencia. Un ejemplo clásico podría ser el
49
Capítulo 6. Test dobles
de una clase que necesita crear una carpeta para almacenar ficheros. Una posible solución podría ser
la de asignar en los métodos setUp y tearDown dicha necesidad. En el caso de setUp, podríamos
verificar que no existe la carpeta y borrarla en el caso de que existiera. En el caso de tearDown, el
efecto contrario, borrar la carpeta. La idea principal es que cada test deje el sistema sin alteraciones
tras su ejecución. El problema de esta solución es que se sigue accediendo a disco y dejando cierta
incertidumbre en la ejecución del test. Además, podemos encontrar problemas adicionales si el disco
tiene algún problema, hay modificaciones no deseadas en el disco, etc...
Para solventar este tipo de situaciones existen librerías como vfsStream. Esta herramienta es un
simulador del sistema de ficheros, el cual ejecuta en memoria todo lo relativo al disco.
class FileSystemCache {
private $dir;
use org\bovigo\vfs\vfsStream;
/**
* @test
*/
public function createsDirectoryIfNotExists() {
$cache = new FileSystemCache(__DIR__ . '/cache');
$cache->store('example', ['bar' => 303]);
$this->assertFileExists(__DIR__ . '/cache');
}
/**
* @test
*/
public function storesDataInFile() {
$cache = new FileSystemCache(__DIR__ . '/cache');
$cache->store('example', ['bar' => 303]);
$this->assertEquals(
['bar' => 303],
unserialize(file_get_contents(__DIR__ . '/cache/example'
);
51
Capítulo 6. Test dobles
}
}
52
Capítulo 7. TDD
Capítulo 7. TDD
Desarrollo guiado por tests (TDD)
7.1 Qué es el TDD
Las siglas TDD (en inlgés Test-driven development), se puede traducir como "desarrollo guiado por
tests". Lo que quiere significar es que el desarrollo del software debe estar guiado por los tests, o
dicho de otra forma, para desarrollar o refactorizar un fragmento de código, debemos escribir los tests
que verifiquen ese comportamiento antes que el código que lo implementa. El concepto Red-Green
testing, viene de que, para crear la funcionalidad final, primero debemos crear un test que no estará
resuelto (Red), y tras la implementación de una solución, el test deberá pasar correctamente (Green).
El desarrollo en TDD está asociado siempre al desarrollo de test unitarios, no de test funcionales o de
integración. El motivo es porque, como veremos a continuación, siguiendo la metodología TDD se
intenta testear el mínimo código posible que completaría una parte de la especificación del problema.
Creación de un test.
Dada una especificación a cumplimentar, creamos un test unitario que debe cumplir la
implementación a desarrollar.
Para cada creación de un nuevo test, ejecutamos el conjunto de tests y verificamos que la
implementación que estamos añadiendo no está resuelta por el estado actual del código, dado que los
test producen el error correspondiente.
Implementamos la funcionalidad, con el mínimo código posible que pase el test. De esta forma,
vamos desarrollando paso a paso una funcionalidad completa, pero con la intención de contemplar
todos los escenarios posibles.
Ejecutamos el conjunto de tests para ver que el código que hemos implementado se comporta de la
forma esperada.
Refactorizar el código
Este paso es importante para, una vez estar seguros de que tenemos una solución para nuestro
problema, debemos dejar el código de la forma más legible y eficiente posible.
Repetir
Los defensores del desarrollo por TDD defiende que siguiendo estos pasos la calidad del código
53
Capítulo 7. TDD
mejor, la cobertura de tests está garantizada y los diseños arquitecturales más meditados. Los
retractores de esta metodología rechazan la necesidad de seguir todos estos pasos, de una forma tan
metódica.
Para nuestro caso, dado un número devolveremos la cadena "fizzbuzz" desde el uno hasta el número
indicado, sustituyendo "Fizz" por "F" y "Buzz" por "B".
Para simplificar nuestro ejemplo, solo en el último test realizaremos la refactorización que hay que
realizar siguiendo los pasos TDD.
El primer test que debemos deberíamos pasar sería la cadena FizzBuzz(1), cuyo resultado debería ser
"1". El test podría ser el siguiente:
Omitimos la devolución del error dado que en este punto, ni siquiera existe la clase FizzBuzz en
nuestro repositorio de código.
class FizzBuzz {
Como se puede apreciar, para pasar este test no hemos intentado resolver el problema completo, sino
que, como nos propone el procedimiento, hemos implementado el mínimo necesario para resolver el
test. En este caso una solución puede ser devolver el mismo valor que el valor de entrada.
54
Capítulo 7. TDD
El siguiente paso será crear un nuevo test, que nos exija añadir funcionalidad a nuestro código. El test
FizzBuzz(2) no pasará, dado que la mínima implementación anterior está preparada para dar el
resultado requerido por la implementación. El test sería:
1) FizzBuzzTest::testTwo
Failed asserting that 2 matches expected '12'.
/home/jose/workspace/MyThings/TestingBook/TddTesting/test/FizzBuzzTest.php:16
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
Como esperábamos, el test no ha pasado. A partir de ahora solo mostraremos la implementación del
método solve. La implementación que pasaría el tests podría ser:
return $str;
}
Siguiendo el procedimiento, ahora añadiríamos un nuevo test para verificar la resolución del
problema para un múltiplo de tres. El test sería:
Y como esperábamos una vez más el test. En la implementación anterior el código no resolvía el caso
de que el número de entrada fuera múltiplo de tres. El error obtenido es:
1) FizzBuzzTest::testThree
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'12F'
+'123'
return $str;
}
En este paso necesitamos añadir la el resultado correcto para números múltiplos de cinco. El test sería
el siguiente:
1) FizzBuzzTest::testFive
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'12F4B'
+'12F45'
return $str;
}
56
Capítulo 7. TDD
Y en último lugar, necesitamos implementar el caso en el que un número sea múltiplo de tres y de
cinco, para ello, creamos el tests para verificar el resultado para el número dieciséis.
1) FizzBuzzTest::testSixteen
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'12F4BF78FB11F1314FB16'
+'12F4BF78FB11F1314F16'
return $str;
}
Dado que el procedimiento TDD contempla el paso de refactorización hemos refactorizado el modoso
solve de la siguiente manera:
57
Capítulo 7. TDD
}
}
return $str;
}
Tras la refactorización hemos vuelvo a ejecutar los tests y obtenido el resultado positivo que
esperábamos.
Incluso si quisiéramos refactorizar nuestro método solve, para permitir que sea configurable, es decir,
poder poner otros números primos como a los que se deba decir otra palabra, con nuestros conjunto
de test podríamos hacerlo garantizando que el comportamiento se mantendrá para las entradas
definidas. Una refactorización de este tipo podría ser de la siguiente forma:
$solution = "";
for($i=1;$i<=$number; $i++) {
$str = [];
foreach($primes as $key => $value) {
if($i%$key==0) {
$str[] = $value;
}
}
if(count($str)>=1) {
$solution.= implode("", $str);
} else {
$solution.= $i;
}
}
return $solution;
}
Esta implementación del método solve pasaría todos los test creados anteriormente y permitiría
distintas opciones en el segundo parámetro de entrada permitiendo más flexibilidad.
Nuestro ejemplo quizás sea un poco exagerado, pero el procedimiento que define TDD es
prácticamente este. Otro punto considerable es la cantidad de código de testeo que se necesita
desarrollar para seguir la metodología. Para el ejemplo anterior, posiblemente utilizando el último test
tendríamos una cobertura de código casi completa, con el requirimiento funcional, aunque no se
habría resuelto el problema considerando todos los casos.
Para algunos desarrolladores es excesivo el tiempo que hay que emplear para desarrollar algo. Sin
embargo, el nivel de detalle al que se puede llegar desarrollando la solución de un problema es muy
detallada.
58
Capítulo 7. TDD
Pensar en el test primero obliga a pensar mejor el interfaz de la aplicación. Este pensamiento en
el interfaz nos ayuda a ver como la clase será usada y a mantener separada dicha interfaz de la
implementación.
Se minimiza el tiempo decicado de depurar el código. Si es necesaria esta depuración sería más
sencilla ya que el resto de la aplicación también está testeada.
Siguiendo TDD somos avisados antes de qué refactorización rompió el estado verde de los
tests.
Permite que el diseño sea más adaptable a los cambios del entendimiento del problema.
Hay que destacar que trabajar con TDD no son todo ventajas. Algunas desventajas es que seguir la
metodología de forma disciplinada no es fácil de aprender. Los desarrolladores que siguen esta
metodología dicen que durante los primeros meses de seguirla, perdieron productividad.
Como veremos en la siguiente sección, BDD es una técnica a nivel de historias de usuarios y a nivel
de la especificación. Phpspec es usada para desarrollar los tests a nivel de especificación o SpecBDD.
En realidad, no hay diferencia entre SpecBDD y TDD, por ello hemos incluido Phpspec dentro de la
sección de TDD y no en la sección de BDD.
El valor añadido de usar una herramienta de especificación en lugar de una herramienta de testeo es
por el lenguaje en sí. Los desarrolladores que optaron por testear su código siguiendo TDD, pronto se
fijaron en la importancia de desarrollar fijándose en el comportamiento y el diseño del código. Por
ello, BDD y herramientas que siguen la filosofía SpecBDD se han centrado en eliminar el vocabulario
de testeo.
Herramientas que basan la filosofía de StoryBDD, como Behat, que también veremos en la próxima
sección, ayudan a entender y clarificar el dominio. Estas herramientas especifican las funcionalidades
de forma narrativa, sus necesidades y cuales son los objetivos. Mientras tanto, con SpecBDD y
herramientas como Phpscpec nos centramos en la implementación.
Para instalar Phpspec en nuestro proyecto a través de Composer necesitamos añadir dependencia
correspondiente:
59
Capítulo 7. TDD
{
"require-dev":{
"phpspec/phpspec": "~2.3"
}
}
Después de añadir las lineas anteriores en nuestro fichero composer.json necesitamos ejecutar el
comando composer install, para instalar las dependencias.
En esta sección vamos a desarrollar un ejemplo utilizando Phpspec. Hay que tener en cuenta que
Phpspec no es solo una herramienta de testeo, sino también de desarrollo, por lo que la el uso es
distinto que el que podemos dar a otros frameworks como PHPUnit.
Supongamos que estamos desarrollando una web en la que usuarios pueden tener un álbum de fotos,
como podría ser Flickr.com. Para dicho sistema necesitaríamos la clase User y la clase Image. Lo
primero que tendríamos que hacer siguiendo Phpspec es crear la especificación de la clase usuario.
Para ello, desde nuestro terminal ejecutaríamos el siguiente comando:
namespace spec;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
Con esta clase estamos creando una especificación, es decir, un test, sobre una clase User. Hay que
tener en cuenta, que en este punto todavía no hemos creado la clase User, por lo que la ejecución de
los tests no pasaría. En este momento podemos ver algunas particularidades de Phpspec, por ejemplo:
Para Phpspec cada método público que comience por it_ es un test, en lugar del prefijo test
necesario en PHPUnit o Atoum.
La clase a testear, en nuestro ejemplo UserSpec es una instancia del SUT User. Esto le da
sentido al punto anterior, dado que para clase de especificación estamos definiendo el
comportamiento, no testeando su implementación.
Los parámetros de entrada de cada tests o especificación (métodos it_) son mockeados
directamente por el framework, como veremos más adelante.
Para continuar con el ejemplo ejecutaremos los tests desde nuestro terminal con el siguiente comando:
60
Capítulo 7. TDD
bin/phpspec run
Dado que la clase User no existe todavía, esta ejecución nos devolverá la siguiente pregunta:
Después de pulsar Y e intro, la clase User.php será creada en la ruta especificada por nuestro
namespace con el código:
class User
{
}
En este punto, si ejecutamos los tests de nuevo veremos que tenemos una respuesta positiva.
El siguiente paso es desarrollar la funcionalidad que permite a un usuario añadir una imagen. Para
ello, en lugar de implementar el método que nos aportaría esta funcionalidad, el método addImage,
crearemos un método nuevo en nuestra clase UserSpec que nos aporte dicha funcionalidad.
$this->shouldHaveCount(1);
}
bin/phpspec run
Como el método addImage ni siquiera existe en nuestra clase User, esta ejecución nos presentaría la
siguiente pregunta:
A continuación pulsamos "Y" y nuestra clase User se modificará con la definición del método que
pasaría el test sin implementar, con la forma:
Para pasar el test, la implementación que deberíamos realizar es sencilla. El método addImage
quedaría de la siguiente forma:
61
Capítulo 7. TDD
La siguiente funcionalidad a implementar podría ser añadir más de una imagen a la vez, por ejemplo
creando un método addImages dentro del la clase User que tenga como parámetro de entrada un array
de imágenes.
$this->getImages()->shouldHaveCount(2);
}
bin/phpspec run
Y Phpspec nos volverá a preguntar por añadir el método addImages y nos dejará la responsabilidad de
implementar dicho método. Pasando directamente a la implementación del método generado por
Phpspec, una solución podría ser:
En el caso anterior no hemos contemplado la posibilidad que algún elemento del array que pasamos
como parámetro al método addImages sea del tipo Image. Para tener en cuenta esta consideración
podemos añadir el siguiente test:
En este test estamos especificando que el método addImage lance una excepción si alguno de los
elementos del array no es de la clase Image. Tras ejecutar el test obtenemos como resultado la
siguiente salida en el terminal:
User
32 X it should be instance of images
expected to get exception, none got.
}
$this->images = array_merge($this->images, $images);
}
El resto del proceso es iterar de la misma forma, hasta acabar con la implementación deseada. Como
vemos, el proceso guiado con Phpspec es prácticamente el mismo que detallamos en la sección de
TDD, siendo la propia herramienta la que crea las clases y métodos cuando no existen y dejándonos la
responsabilidad de implementar dichos métodos, para pasar los tests especificados. En el ejemplo solo
hemos dado visibilidad a una pequeña parte de las posibilidades del framework. Algunas de las
posibilidades que, por la simplicidad del ejemplo, nos estamos saltando son:
Especificación de tipo (Type Matcher): Con este tipo de métodos se puede concretar el tipo de
un objeto, la devolución de un método, si es una instancia de una clase o si implementa un
interfaz.
Especificación de excepciones (Throw Matcher): Especificamos los casos o situaciones por las
que devolvemos una excepción, como hemos visto en el ejemplo al introducir un argumento
incorrecto.
Identificación del estado de objeto (ObjectState Matcher). Testea el valor del estado de un
objeto llamando métodos del propio objeto. Estos métodos deberán comenzar con is o has y
devuelven true o false.
Identificador de typo (Scalar Matcher). Especifica el tipo de datos del valor devuelto por el
método.
Estos pueden ser los "Matcher", las formas de determinar el comportamiento de una implementación
más comunes, pero hay otras: ArrayKeyWithValue, ArrayKey, StringStart, StringEnd, StringRegex.
Además, se pueden extender otros Matcher utilizando un Inline Matcher. En la documentación de
Phpspec [23] podemos ver este ejemplo de Inline Matcher, en el que se determina un valor por
defecto para el atributo username la clase Movie:
namespace spec;
use PhpSpec\ObjectBehavior;
use PhpSpec\Matcher\InlineMatcher;
$this->getOptions()->shouldHaveValue('diegoholiveira');
}
64
Capítulo 8. BDD
Capítulo 8. BDD
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.
El desarrollo guiado por comportamiento especifica que tests de unidades de software deben
cumplimentar unidades de comportamiento deseado.
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:
Narrativa
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.
Una descripción de cada caso específico de la narración. Este escenario tiene la siguiente estructura:
65
Capítulo 8. BDD
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).
66
Capítulo 8. 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.
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.
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
67
Capítulo 8. BDD
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.
wordpress:
image: wordpress
links:
- db:mysql
ports:
- 8080:80
db:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: example
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:
68
Capítulo 8. BDD
{
"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/"
}
}
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
@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"
69
Capítulo 8. BDD
@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.
70
Capítulo 8. BDD
*/
public function __construct(array $parameters)
{
// Initialize your context here
}
/**
* @Given /^espero (\d+) segundos?$/
*/
public function esperoSegundo($arg1)
{
sleep($arg1);
}
}
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.
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:
expect(false)->not()->toBe(false);
});
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:
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() {
72
Capítulo 8. BDD
ArrayObject
->count()
Ok should return the number of items
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.
73
Capítulo 9. Codeception: Solución todo en uno
Codeception está compuesto por Actors. Los Actors son clases autogeneradas de tres posibles tipos:
UnitTester, FunctionalTester y AcceptanceTester. Cada una de estas clases tiene métodos predefinidos
que intentan resolver problemas comunes de testeo. Estas clases pueden ser configuradas mediante
ficheros de configuración en formato Yaml. Entre algunos de los problemas que resuelven está el de
rellenado de una base de datos tras la ejecución de cada test, el uso del patrón PageObject [47] para
test de aceptación y funcionales, etc.
La parte fuerte de Codeception es que es un framework pensado para cubrir los distintos tipos de test
dentro de la misma herramienta. Para ello, y a diferencia de otros frameworks, la propia herramienta
crea la estructura de carpetas donde alojar los distintos tipos de tests ejecutado el commando:
vendor/bin/codecept bootstrap
tests/acceptance/
tests/functional/
tests/unit/
Dentro de las distintas herramientas para testear Codeception es la que de forma más explícita aborda
el problema de distintos tipos de testeo de forma conjunta. Si pensamos en el resto de herramientas
que hay en el mercado, PHPUnit está enfocada a realizar test unitario, aunque se puedan realizar test
de cualquier tipo utilizando librerías complementarias. Con Phpspec [23] podemos realizar test
unitarios en formato BDD, pero no podemos realizar tests de integración o aceptación. Behat [24] está
enfocado principalmente a tests BDD de aceptación. Atoum [7], al igual que PHPUnit, está también
orientado para realizar tests unitarios.
Implementación del patrón PageObject, el cual nos permite asociar a atributos de un objeto
74
Capítulo 9. Codeception: Solución todo en uno
definiciones del documento HTML mediante XPath para ser usados en los tests. Comparando
con Behat, esto debía ser indicado en la especificación Gherkins o implementando en la clase
FeatureContext, después de interpretar el fichero Gherkins.
Codeception permite la definición de escenarios dentro de los tests unitarios. Esta funcionalidad
no está implementada en PHPUnit y mejora la legibilidad de los tests, al permitir agrupar
aserciones dentro de una misma especificación a cumplimentar.
Codecepcion soporta posee módulos específicos para realizar test funcionales en los
frameworks más comunes (Symfony2, Laravel 4 y 5, Zend 1.x y 2, Yii1, Yii2 y Phalcon). Esto
nos permite crear test funcionales solo especificandole en la configuración con qué framework
estamos trabajando y Codeception se encargará de utilizar las dependencias propias del
framework para ejecutar los controladores a testear.
9.4 Instalación
Aunque existen otros métodos de instalación y uso de Codeception, recomendamos el mismo método
que hemos utilizado a lo largo de este PFC a través de Composer. Para ello debemos incluir la
dependencia de Codeception en nuestro fichero composer.json de la forma:
{
"require": {
"codeception/codeception": "^2.1"
}
}
Para crear nuestros tests usando Codeception crearemos una carpeta CodeceptionTesting, incluiremos
el fichero composer.json con su dependencia y ejecutaremos el comando:
vendor/bin/codecept bootstrap
Este comando nos creará todas las configuraciones por defecto para poder realizar el test deseado.
75
Capítulo 9. Codeception: Solución todo en uno
Dado que vamos a realizar sólo tests de aceptación con Selenium, editaremos el fichero
tests/acceptance.suite.yml y cambiaremos PhpBrowser, que viene por defecto, por WebDriver, he
indicaremos la url donde responderá nuestro WordPress:
class_name: AcceptanceTester
modules:
enabled:
- WebDriver:
url: http://localhost:8080/
- \Helper\Acceptance
Dado que vamos a testear tres escenarios, vamos a crear las tres clases para ello utilizando el
comando generate:cetp de la forma:
tests/acceptance/WordpressAddPostCept.php
tests/acceptance/WordpressDeletePostCept.php
tests/acceptance/WordpressLoginCept.php
$I = new AcceptanceTester($scenario);
$I->wantTo("Login to my blog");
$I->amOnPage("/wp-login.php");
$I->fillField("#user_login", "jose");
$I->fillField("#user_pass", "testing");
$I->click("wp-submit");
$I->see("Dashboard");
Dado que por convenio Codeception recomienda utilizar la variable "I" para crear un escenario
concreto, la lectura del código es muy intuitiva, cercana al lenguaje natural, salvando el hecho de que
sigue siendo código PHP. De hecho, si ejecutáramos el comando "vendor/bin/codecept
generate:scenarios acceptance" podríamos ver una descripción en inglés simple de nuestros tests de
aceptación.
vendor/bin/codecept run
Por el momento solo tenemos implementación para el proceso de login y como podemos ver ha
pasado correctamente. También podemos ver la configuración de los otros conjunto de tests y como
no hemos creado ningún tests, nos lo indica en las secciones correspondientes.
Para testear el escenario en el que un usuario escribe un post en el blog, vamos a realizar una pequeña
refactorización y trasladaremos la lógica del proceso de login a un StepObject. Para ello creamos una
de estas clases ejecutando el comando:
namespace Step\Acceptance;
$I->fillField('#user_login', $user);
$I->fillField('#user_pass', $password);
$I->click('wp-submit');
}
}
Ahora podemos refactorizar nuestro test de aceptación para el proceso login, utilizar la clase que
hemos creado como StepObject. El código para esta refactorización sería:
use Step\Acceptance\Login;
$I = new Login($scenario);
$I->login('jose', 'testing');
$I->see('Dashboard');
use Step\Acceptance\Login;
$I = new Login($scenario);
$I->login('jose', 'testing');
$I->see("Post published");
Y de la misma forma, el código para testear la eliminación del post recién insertado sería:
use Step\Acceptance\Login;
$I = new Login($scenario);
$I->login('jose', 'testing');
$I->amOnPage('/wp-admin/edit.php?post_status=trash&post_type=post');
$I->checkOption(['name'=>'post[]']);
$I->selectOption("action", "Delete Permanently");
$I->click("#doaction2");
$I->see("1 post permanently deleted");
Como podemos ver, la sintaxis del código para testear tests de aceptación utilizando Codeception es
muy legible, incluso para una persona no dedicada a la programación, pero no es tan intuitivo como la
forma de escribir este mismo tipo de test con el conjunto Behat y Gherkins. Por otra parte, la
modularidad del código que nos permite Codeception, con posibilidades como las que nos ofrece el
proceso implementado por la clase Login es una ventaja considerable. Ambas herramientas tienen
ventajas e inconvenientes a ser tomados en cuenta para elegirlos, aunque no son incompatibles entre
sí.
78
Capítulo 10. Testeando APIs
En el mundo web, una API no es más que una forma de permitir que un entorno distinto de la
aplicación pueda acceder a los datos a través de un lenguaje común. Este tipo de comunicación
normalmente de desarrolla sobre el protocolo HTTP, dado que es el más extendido en internet.
Hay varias tecnologías para permitir dicha comunicación, como por ejemplo SOAP (en inglés, Simple
Object Access Protocol) o REST (en inglés, Representational State Transfer). Dado que el tipo de API
más extendido, tanto para aplicaciones móviles como para entornos web, es REST, este capítulo lo
centraremos en este tipo.
Algunos ejemplos claros de estos servicios pueden ser pasarelas de pagos como PayPal, almacenaje
de ficheros como Amazon S3, o integración de datos de servicios de analítica como Google Analytics.
Dado que es muy común tener este tipo de integraciones, es conveniente tener estas piezas de código
bajo test.
Otra forma de realizar estos test es ejercitando API de forma completa. Estos tests son de integración
o funcionales y no se pueden considerar como tests unitarios, dado que no podríamos controlar el
estado de la aplicación y tienen varias complejidades. Es necesario que la máquina que ejecuta los
tests tenga conexión a internet, para conectarse con el servicio sobre el se realizarán los tests.
A continuación veremos un ejemplo de una implementación de una clase que pueda conectarse a la
API pública de un proyecto y sus correspondientes tests, tanto funcionales como unitarios.
En nuestro ejemplo vamos a implementar una clase llamada "Flickr" que sea capaz de conectarse con
Flickr.com y nos permita interactuar.
Para poder utilizar la API de Flickr.com es necesario tener una cuenta de usuario y registrar una
79
Capítulo 10. Testeando APIs
aplicación para conseguir las claves de acceso. En el caso de Flickr.com, y generalmente a cualquier
API que permita autenticarse a través del protocolo Oauth [29], necesitaremos una clave de usuario y
una clave secreta.
El nuestra implementación de la API de Flickr.com hemos utilizado tres librerías de código abierto:
oauth1-client [32], como cliente del protocolo Oauth 1.0, usado Flickr.com para permitir que
aplicaciones de terceros accedan a los datos de usuarios.
Con estas tres librerías solucionamos los distintos problemas que nos encontramos y nos dejan con
una sencilla clase para poder gestionar los métodos básicos de la API de Flickr.com. El código de
nuestra clase de Flickr.com sería el siguiente:
namespace Flickr;
use Noodlehaus\Config;
use Guzzle\Service\Client as GuzzleClient;
class Flickr
{
protected $client;
protected $config;
private $tokenCredentials;
$this->tokenCredentials = $token;
}
$result = json_decode($response->getBody());
return $result;
}
$temporaryCredentials = $this->server->getTemporaryCredentials();
return $temporaryCredentials;
}
$this->tokenCredentials = $this->server
->getTokenCredentials($tempCredential, $oau
return $this->tokenCredentials;;
}
if($isWithOauth) {
$ouathParams = ['auth_token'=> $this->tokenCredentials->getIdentif
'api_sig' => $this->tokenCredentials->getSecret()]
81
Capítulo 10. Testeando APIs
$optionsUrl = http_build_query($options);
return self::URL_FLICKR.
$method.
'&api_key='.
$this->config->get('flickrKey').
'&'.$optionsUrl;
}
}
Además, necesitamos un fichero de configuración donde tengamos guardadas las distintas claves
necesarias para acceder a Flickr.com, tal y como lo especificamos en la propia clase Flickr y es leído
por la librería hassankhan/config. El contenido de este fichero de configuración debería deter la
forma:
flickrKey: CHANGE_WITH_YOUR_KEY
secretFlickr: CHANGE_WITH_YOUR_SECRET
callbackUriFlickr: http://dev.local
Donde, en caso de utilizar esta librería para conectarse con una cuenta de aplicación de Flickr.com,
deberíamos de sustituir CHANGE_WITH_YOUR_KEY, CHANGE_WITH_YOUR_SECRET y
CHANGE_WITH_YOUR_SECRET por los valores correspondientes.
Esta pequeña clase no implementa todos los métodos de Flickr.com, dado que hay algunos métodos
especiales como la carga de fotos que necesitan una acción especial en Flickr.com, tal y como lo
indican en la documentación oficial https://up.flickr.com/services/upload/.
Hay dos métodos que merezca la pena comentar. El método __construct, que inicializa las
dependencias en el caso de que no vengan como parámetros. Esta es una buena práctica si queremos
que nuestra clase pueda ser gestionada por un inyector de dependencias y pueda ser fácilmente
testeable, como veremos a continuación.
El siguiente método a comentar es callMethod. Este método es el que realza la petición HTTP a
Flickr.com*, utilizando el método de llamada. Los distintos métodos que soporta Flickr son muy
variados y permiten tanto acceder como modificar distintos elementos en la cuenta de usuario.
Dado que nuestro método es genérico, vamos a realizar dos tests distintos: Vamos a testear los
métodos de la API Flickr.com flickr.photos.search y flickr.galleries.create. Realizaremos dos veces el
mismo test. Un test realizando la ejecución de la API y otra vez mockeando todas las dependencias y
utilizando un fixture, como si tuviéramos el resultado de la ejecución de la API. De esta forma
daremos visibilidad a las dos posibles aproximaciones que podemos encontrar cuando testeamos un
API pública de un servicio online.
/**
* Test a query of Flickr without mocks.
*/
82
Capítulo 10. Testeando APIs
$this->assertEquals(100, count($photoCats->photos->photo));
}
Este test inicializa nuestra implementación del cliente de la API de Flickr.com y realiza una búsqueda
con el texto "cats". Como esta búsqueda es muy popular, verificamos que tenemos 100 fotos, que es
el número de fotos que devuelve Flickr.com por defecto.
Este test tiene varios problemas y ventajas: Si la máquina en la que se ejecuta no puede conectarse a
Flickr.com por algún motivo, el test fallará. Además, al realizar una conexión HTTP y una búsqueda
en los sistemas de Flickr.com, será lento en ejecución. Si quisiéramos verificar el contenido de alguna
foto, el test fallaría con el paso del tiempo, dado que los resultados para una búsqueda en Flickr.com
cambiarán según los usuarios vayan añadiendo más fotos. Por otra parte, este test está ejecutando
completamente nuestra implementación y verificando que, tanto nuestro código como Flickr.com se
comportan de la forma esperada.
/**
* Test a query of Flickr with mocks.
*/
public function testQueryWithMocks()
{
$client = $this->prophesize(Client::class);
$response = $this->prophesize(Response::class);
$response->getBody()->willReturn($this->getFixtureContent());
$request = $this->prophesize(RequestInterface::class);
$request->send()->willReturn($response->reveal());
$client->get(Argument::any())->willReturn($request->reveal());
$server = $this->prophesize(FlickrServer::class);
$flickr = new Flickr('config.yml', $client->reveal(), $server->reveal());
$this->assertEquals(100, count($photoCats->photos->photo));
}
Este test es el mismo que el anterior, pero con grandes diferencias conceptuales. Dado que estamos
inyectando mocks en el constructor del cliente HTTP Guzzle, el test nunca llega a conectarse a
Flickr.com. Por ello, es un test más rápido en ejecución. Para que funcione el tests hemos necesitado
guardar una búsqueda, almacenada en el fichero "./tests/fixtures/cats.json" de nuestro proyecto. Como
83
Capítulo 10. Testeando APIs
contrapartida, con este test no llegamos a ver claramente que nuestra implementación está dando el
resultado esperado, dado que no estamos ejercitando las dependencias, de la misma forma que un
cambio en Flickr.com pasaría sin ser detectado por nuestros tests.
Los dos ejemplos anteriores ejecutaban un método por el que no es necesario autentificarse. Ahora
veamos un método en el que es necesario estar a autentificado y los distintos problemas que esto
conlleva.
/**
* Test create Galery without mocks.
*/
public function testCreateGallery()
{
$token = new TokenCredentials();
$token->setIdentifier('IDENTIFIER');
$token->setSecret('SECRET');
$this->flickr->setTokenCredentials($token);
$gallery = ['title' => 'Galería de ejemplo', 'description' => 'Galería de
$res = $this->flickr->callMethod('flickr.galleries.create', $gallery,
$this->assertEquals('ok', $res->stat);
}
Al igual que el test testQueryDirect, este test se conecta con Flickr.com y realiza la acción de crear
una galería. Por ello podemos decir que tiene los mismos problemas que el test testQueryDirect, pero
en realidad tiene dos problemas más. Como podemos ver, en el test hemos tenido que crear un objeto
TokenCredentials, asignarle los valores Identifier y Secret a través de los setter correspondientes he
inyectar este objeto a nuestro objeto Flickr.com. Esto debemos hacerlo por como funciona Oauth.
Los valores IDENTIFIER y SECRET debemos configurarlos realizando una conexión real a
Flickr.com, aceptar el uso de la API desde nuestra aplicación y obtener la respuesta. Este proceso es
tedioso y complejo, además de inestable, dado que si en algún momento indicamos que ya no damos
permiso a nuestra aplicación a conectarse a Flickr.com, el test dejará de funcionar.
Además, este test presenta otro problema. Cada vez que ejecutamos el test estamos creando una
galería nueva. Esto se puede comprobar en el perfil de usario asociado a las credenciales. Dado que
Flickr.com no soporta un método para el borrado de galerías y automatizar el borrado en el mismo
test, la cuenta del usuario quedará con varias galerías con el nombre indicado en el test.
Hay que destacar que Oauth1 es bastante complejo de testear, debido a la autorización por parte del
usuario al acceso de una tercera aplicación a los servicios que permite acceder. En testeo unitario esto
es prácticamente imposible, debido a que hay una interacción a tres bandas. En un test de aceptación
con herramientas como Behat con Selenium o alguna otra herramienta este tipo de test se podría
automatizar, pero de tal y como lo estamos resolviendo, es necesario obtener las credenciales
manualmente para poder ejecutar estos tests.
84
Capítulo 10. Testeando APIs
Veamos una implementación utilizando mocks, y por lo tanto, exenta de estos dos últimos problemas:
/**
* Test create Galery with mocks.
*/
public function testCreateGalleryWithMocks()
{
$client = $this->prophesize(Client::class);
$response = $this->prophesize(Response::class);
$response->getBody()->willReturn($this->getFixtureContentForCreateGallery(
$request = $this->prophesize(RequestInterface::class);
$request->send()->willReturn($response->reveal());
$client->post(Argument::any(), Argument::any())->willReturn($request->reve
$server = $this->prophesize(FlickrServer::class);
$server->getHeaders(Argument::any(),
Argument::any(),
Argument::any(),
Argument::any())->willReturn([]);
$this->assertEquals("ok", $res->stat);
}
Este caso presenta un escenario muy similar al test testQueryWithMocks, pese a que el método que
estamos testeando necesita autenticación. Como todo está mockeado y la respuesta inyectada en otro
fixture el test no tiene efectos secundarios en ninguna cuenta de Flickr.com y no hemos necesitado
conectarnos y aceptar el uso de la aplicación previa a la creación al test.
Por otra parte, y como comentábamos con el test de la búsqueda, este tipo de test no realiza nada, por
lo cual no podemos garantizar que la acción en los servicios de Flickr.com sea válida.
Optar por una u otra forma de realizar test a servicios externos depende de la naturaleza del problema
y es decisión del equipo de desarrollo. En la mayor parte de los casos la mejor solución, por
simplicidad y tiempo de ejecución sea la de mockear cualquier sistema externo, pero hay situaciones
concretas y críticas, como sistemas de pagos, en las cuales la mejor solución es testear el sistema de
forma completa. Algunos de estos servicios online, como Paypal.com son conscientes de la necesidad
de crear tests validos y ofrecen entornos de pruebas como comentábamos anteriormente, para que los
desarrollos sobre esta plataforma estén implementando de la forma más estable y con mejores
85
Capítulo 10. Testeando APIs
garantías posibles.
86
Capítulo 11. Integración continua
Tanto en equipos con varios desarrolladores, como en proyectos de código abierto, los cambios a los
que está sujeto un proyecto son constantes y generalmente rápidos, por lo que introducir herramientas
de monitorización y mejora continua en la calidad del software ayudan a que los equipos mantengan
unos varemos de calidad aceptables. Esta práctica es ventajosa tanto para la comunidad que está
trabajando en el proyecto, como para los dueños del proyecto.
La integración continua reduce la cantidad de procesos repetitivos que los desarrolladores necesitan
realizar cuando construyen o lanzan el software en el que están trabajando, y por lo tanto, reducen el
riesgo o se adelantan en la detección de errores.
Un desarrollador finaliza una tarea y el cambio es integrado con el resto del proyecto.
Con este sistema, la detección de errores queda automatizada y los errores localizados nada más un
desarrollador haya terminado una tarea.
En el caso de que un proyecto tenga un conjunto de tests de integración grande, ejecutar los tests
después de cada commit puede no ser una solución viable, dado que los tests funcionales son lentos.
Para solventar esta situación, y otras situaciones especiales, las herramientas de integración continua
pueden ser configuradas con gran cantidad de detalles.
87
Capítulo 11. Integración continua
sistemas online también contamos con Travis CI y otras alternativas como continuousphp, Bamboo,
Codeship, etc.
En el mercado hay multitud de proyectos para solventar el problema de integración continua. Hemos
agrupado las distntas opciones en función de los ejemplos que mostraremos a continuación. Para tener
una visión más global y tener referencia más detallada podemos consultar el artículo de Wikipedia en
inglés "Comparison of continuous integration software" [33], donde podemos ver una tabla con
multitud de proyectos de integración continua clasificados por plataforma, licencia, si consta de
builder para windows, sistema de notificación, integración con IDEs y otro tipo de integraciones.
Para ilustrar la forma de configurar un proyecto hemos decidido utilizar dos alternativas: Una
instalación de Jenkins y la versión online de Travis CI para proyectos de código libre. Travis CI es
gratuito para proyectos publicados bajo licencias de código abierto, por lo que podremos utilizarlo sin
problemas.
11.4 Jenkins
Pese a que Jenkins [34] es un proyecto escrito en Java, la comunidad PHP lo ha aceptado y utilizado
como herramienta de integración continua, ejecutando tanto PHPUnit como otras herramientas para
monitorizar la calidad de código [45]. Algunas de estas herramientas son:
PHP_Depend: Analizador de código que genera determinadas métricas para asegurar la calidad
de código, he indicar posibles refactorizaciones según dichas métricas.
PHP Mess Detector: Analizador de código que determina secciones de código muerto, posibles
bugs o expresiones complejas.
PHP Copy/Paste Detector: Herramienta que encuentra fragmentos de código duplicado en una
base de código.
Lo primero que necesitamos es descargar el fichero binario ejecutable de Jenkins en la página oficial
y ejecutar el comando:
Para instalar y ejecutar Jenkins hay otras alternativas, como instalar un contenedor utilizando Docker
y utilizar la imagen correspondiente. Este proceso sería similar a la instalación que hicimos en el
capítulo 8 con la instalación de WordPress" utilizando Docker*.
Otra alternativa en máquinas con la distribución Linux Ubuntu instalada podemos utilizar los paquetes
oficiales siguiendo los comandos:
88
Capítulo 11. Integración continua
Para más información sobre la instalación recomendamos visitar la documentación oficial Jenkins
[34].
Una vez instalado Jenkins podemos acceder a la aplicación abriendo un navegador y accediendo a la
url http://localhost:8080, que es la configuración por defecto.
Dado que vamos a configurar nuestro proyecto XStrings utilizando GitHub [10], necesitamos tener
instalado Git en la máquina donde ejecutaremos Jenkins. Además, necesitamos tener instalado
Composer [11], dado que seguimos el estándar PSR-4 [2] para autolading, y las dependencias de
testing de nuestro proyecto.
Jenkins es perfectamente configurable para desarrollos PHP, puesto que la comunidad ha creado
multitud de plugins. En nuestro ejemplo haremos uso de algunos plugins. Para hacer uso tenemos que
visitar la sección de configuración de Jenkins de la ejecución anteriormente lanzada bajo la URL
http://localhost:8080, marcar los plugins deseados y pulsar el botón "Install without restart". Después
de que todos los plugins estén instalados necesitamos reiniciar Jenkins.
Clover PHP plugin: Lo utilizaremos para integrar los reportes en formato Clover.
GIT plugin: Necesitaremos este plugin para acceder a Github, descargar el código y detectar
nuevas versiones.
Para configurar nuestra librería, lo primero que necesitamos es crear un proyecto en nuestro Jenkins.
Para ello daremos al link "New Item". A continuación rellenaremos los datos necesarios para integrar
nuestro proyecto:
Estas acciones las configuraremos añadiendo nuevas secciones pulsando en "Add build step",
seleccionando "Execute shell" y rellenando cada formulario con el comando correspondiente.
Por último crearemos las acciones posteriores a la ejecución de nuestra integración, en nuestro caso
89
Capítulo 11. Integración continua
hemos decidido:
Para crear estos pasos de post-proceso pulsaremos en "Add post-build action" y seleccionaremos la
correspondiente del desplegable de acciones.
Con esta configuración, cada vez que realicemos una modificación sobre la rama master de nuestro
proyecto, Jenkins lanzará el proceso de integración y podremos ver los resultados.
Jenkins PHP [46] es una web especializada en información para configurar Jenkins como sistema de
integración continua para proyectos en PHP. Está desarrollado por Sebastian Bergmann, creador de
PHPUnit. En esa web explican cuales son los plugins necesarios para integrar un proyecto en PHP así
como una plantilla de ejemplo basada en Apache Ant para configurar de una forma completa nuestro
servidor Jenkins.
11.5 Travis CI
Travis CI [35] es un proyecto de integración continua con varias versiones. Una versión de código
libre alojada en https://travis-ci.org, en la que podemos configurar cualquier proyecto de código libre
publicado en GitHub. Y otra versión de pago, alojada en http://travis-ci.com, en la que podemos
testear cualquier repositorio privado. En esta sección nos centraremos en la versión gratuita para
proyectos de código libre.
La principal ventaja por la que utilizar una herramienta como Travis CI es el ahorro que sopone la
instalación y configuración de un proyecto bajo integración continua. Además, como Travis CI trabaja
en un entorno basado en contenedores, crear configuraciones específicas relativamente complejas es
trivial, frente a la instalación de Jenkins. Por ejemplo, supongamos que el proyecto que queremos
poner bajo integración continua es un proyecto de código abierto y queremos que sea funcional en la
mayor parte de versiones de PHP posibles. Utilizando Jenkins necesitaríamos instalar las distintas
versiones de PHP, lo cual no es un trabajo simple, y crear el proceso de integración con cada una de
las ejecuciones. Para Travis CI, pese a que es igual de complejo, la herramienta lo proporciona de
forma transparente y sencilla.
Trabajar con Travis CI no son todo ventajas. Las dependencias o versiones con las que podemos
trabajar vienen dadas por la herramienta y no es posible escoger otra, que en determinados casos
podría ser necesaria. A fecha de hoy, la versión más antigua de PHP soportada por Travis CI es 5.3.8,
he integrar un proyecto con una versión anterior podría ser no posible o producir problemas.
Además, herramientas de análisis estático de código, reportes de cobertura de código, etc,.. no son
proporcionadas por Travis CI, aunque existen otros proyectos que pueden complementarlo, que dan
soluciones especializadas para cada parte de la integración continua.
Para crear un proyecto en Travis CI, necesitamos tener una cuenta en Github.com y crear una cuenta
en Travis CI asociada a la de GitHub a través del protocolo de autentificación Oauth [29] (visto en el
capítulo 10 "Testeando API".
Una vez aceptado el acceso a GitHub por parte de Travis CI, seremos redireccionados a una página de
90
Capítulo 11. Integración continua
Travis CI en la que veremos nuestros repositorios de GitHub. Clickando en los distintos botones
On/Off habilitaremos la integración del proyecto deseado en Travis CI. Además, necesitamos
configurar los "Services Hooks" en nuestra configuración de cada proyecto, para que en GitHub
aparezcan las notificaciones de estado de la integración continua.
Para que se produzca la ejecución de Travis CI, necesitamos configurar el contenedor donde se
producirá la integración en la forma que necesita nuestro proyecto. Los contenedores de Travis CI
tienen con la mayor parte de los paquetes necesarios para ejecutar un proyecto de integración para
PHP. Al igual que hicimos con Jenkins, necesitamos instalar las dependencias y crear el autoloading a
través de Composer. Esto, y el resto de configuraciones lo especificaremos en el fichero .travis.yml,
que debe estar en la raiz de nuestro proyecto.
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
before_script:
- composer install --dev
notifications:
email:
- example.mail@example.com
La integración será ejecutada para las versiones PHP5.4, PHP5.5, PHP5.6 y PHP.7. Como
comentábamos anteriormente, este proceso en Jenkins sería tedioso puesto que tendríamos que
realizar al instalación de cada una de las versiones en la máquina donde ejecutar el proceso de
integración.
Como podemos ver, teniendo una cuenta de Travis CI y el proyecto a integrar alojado en GitHub,
configurar un entorno de integración continua consiste solamente en editar un pequeño fichero en
formato Yaml y situarlo en la raíz de nuestro proyecto. La comodidad que ofrece este tipo de servicios
frente a la alternativa instalada por nosotros es claramente visible, aunque la perdida de flexibilidad y
posibilidades que ofrecen configuraciones propias en Jenkins también son considerables.
91
Capítulo 11. Integración continua
92
Capítulo 12. Test de proyectos existentes
Para hacer un análisis más profundo de la calidad del código de testeo de los proyectos que
escogeremos, utilizaremos una herramienta específica para mutación de tests: Humbug.
12.1 Humbug
Humbug [36] es un framework para mutación de tests, con el objetivo de medir la eficacia real del
conjunto de tests con el objetivo de mejorarlo.
Humbug realiza un análisis del código y, de forma dinámica, realiza pequeñas mutaciones con el
objetivo de que los tests no pasen. Esto indicará que el tests está bien pensado he implementado. Dado
que los tests están pensados para prevenir una regresión, imponer al sistema una regresión real que no
sea detectada por nuestros tests implica que algo no está tan bien como parecía.
Hay que tener en cuenta que Humbug está en modo experimental, por lo que el resultado del análisis
que haremos a continuación debe ser considerado como un experimento.
Para usar Humbug debemos instalarlo en el proyecto que deseemos testear. Esto puede realizarse
mediante la instalación de un fichero .phar en el sistema, o bien mediante Composer [11] de forma
global en el sistema:
{
"timeout": 10,
"source": {
"directories": [
"src"
]
},
"logs": {
"text": "humbuglog.txt",
"json": "humbuglog.json"
}
}
Mutación asesinada (.): Una mutación ha causado que el test falle, lo cual es una salida positiva.
Mutación escapada (E): Una mutación no ha conseguido que el test falle, por lo cual la
consideramos como un error, dado que esperamos que un test falle para un cambio de código.
Mutación no cubierta (S): Una mutación que ocurre en una linea no estaba cubierta por ningún
test unitario. Dado que no tiene test, consideramos esta salida como errónea.
Fatal Error (F): Una mutación ha generado un fatal error. Estas situaciones pueden deberse a
errores del propio Humbug, o a una salida positiva en nuestro entendimiento, dado que la
mutación a provocado una inestabilidad en la aplicación.
Timeout (T): Esto se debe a que el test unitario se excede en tiempo de ejecución configurado
por Humbug. Lo consideramos como una salida positiva dado que un timeout debe ser un
comportamiento inapropiado, a veces provocado porque una mutación acaba creando un bucle
infinito.
Puntuación de indicación de mutaciones (Mutation Score Indicator) es 47%, esto significa que
el 47 de todas las mutaciones generadas fueron detectadas (por ejemplo, asesinadas, timeouts o
errores fatales). Si la covertura de tests fuera 65%, esto indica que tenemos un 18% de tests
inestables.
Covertura de código por mutaciones (Mutation Code Coverage) es de 67%. En teoría debería
de ser igual a la covertura de código generada por los tests, pero la covertura de tests ignora las
mutaciones.
Mutaciones:
En la página oficial de Humbug se puede ver todos los tipos de mutaciones que puede realizar. Para
ilustrar y ayudar a entender los cambios que realiza en el código añadimos la tabla 12.1 de mutaciones
de lógica binaria:
Original Mutado
true false
false true
&& \ \
\ \ &&
and or
or and
!
: Mutaciones binarias
12.2 Silex
Silex [37] es un micro-framework desarrollado en PHP utilizando componentes de Symfony2 y Pimple
como inyector de depencencias e inspirado en sinatra (framework desarrollado en Ruby).
Añadimos Silex en este apartado por entender que es una versión simplificada de los desarrollos que
está realizando el equipo de Symfony2.
94
Capítulo 12. Test de proyectos existentes
Para comenzar a explorar en los tests de Silex lo primero que necesitamos es descargar el código e
instalar las dependencias utilizando Composer:
Si ejecutamos los tests unitarios con la opción para ver la cobertura de tests obtendremos el resultado:
Cobertura de tests: 86.19% (1279 / 1484) Funciones y métodos: 81.92% (145 / 177) Clases y Traits:
52.17% (24 / 46)
La cobertura de código en lineas es bastante alta. Se considera de forma general que un proyecto con
una cobertura de tests por encima del 70% está en una situación favorable.
Para ver si estos tests son lo eficientes que deberían ser, ejecutemos Humbug y veamos el resultado
que obtenemos:
Humbug running test suite to generate logs and code coverage data...
M.M...MM....E...MMMM..MMMM..M..S.E...SM..M.......MM.E....M.M | 60 (12/48)
.S......MMMM..........EEE.SSE......E.M.........M............ | 120 (15/48)
....EM....MM...EE.......E.MEE...MMMMMMMS.................... | 180 (16/48)
.....M..ME.MMME...ESS..M..S..S..SS...SSSSSSSSSSSSSS.EEE..... | 240 (32/48)
..E.................M.........M....ESSMMM..............S..M. | 300 (38/48)
..E.E.M...M...SSS..................EM.MMM.M...E
Metrics:
95
Capítulo 12. Test de proyectos existentes
Remember that some mutants will inevitably be harmless (i.e. false positives).
Viendo el resultado de Humbug, tenemos un 75% de MSI, mientras que la cobertura era del 86%, por
lo cual tenemos un 11% de los tests que no han respondido correctamente a una mutación. Teniendo
en cuenta que según la propia documentación de Humbug, es posible encontrar falsos positivos,
parece un resultado bastante coherente.
Silex utiliza las herramientas ofrecidas por PHPUnit para mockear, aunque hay relativamente pocos
mocks. De las 59 clases de tests, 20 utilizan algún tipo de mock.
Silex está integrado utilizando la herramienta online Travis CI. Esto podemos saberlo mirando el
fichero de configuración .travis.yml, situado en la raiz del proyecto.
language: php
sudo: false
cache:
directories:
- $HOME/.composer/cache
before_script:
# symfony/*
- sh -c "if [ '$TWIG_VERSION' != '2.0' ]; then sed -i 's/~1.8|~2.0/~1.8/g'
composer update; fi"
- sh -c "if [ '$SYMFONY_DEPS_VERSION' = '3.0' ];
then sed -i 's/~2\.3|3\.0\.\*/3.0.*@dev/g' composer.json; composer upd
- sh -c "if [ '$SYMFONY_DEPS_VERSION' = '2.8' ];
then sed -i 's/~2\.3|3\.0\.\*/2.8.*@dev/g' composer.json; composer upd
- sh -c "if [ '$SYMFONY_DEPS_VERSION' = '' ];
then sed -i 's/~2\.3|3\.0\.\*/2.7.*@dev/g' composer.json; composer upd
- sh -c "if [ '$SYMFONY_DEPS_VERSION' = '2.3' ];
then sed -i 's/~2\.3|3\.0\.\*/2.3.*@dev/g' composer.json; composer upd
- composer install
script: phpunit
matrix:
include:
- php: 5.3
- php: 5.4
- php: 5.5
- php: 5.6
96
Capítulo 12. Test de proyectos existentes
env: TWIG_VERSION=2.0
- php: 5.6
env: SYMFONY_DEPS_VERSION=2.3
- php: 5.6
env: SYMFONY_DEPS_VERSION=2.8
- php: 5.6
env: SYMFONY_DEPS_VERSION=3
- php: 7.0
- php: hhvm
Viendo el fichero de configuración podemos apreciar que Silex funcionará para las versiones de PHP
desde la 5.3 hasta la 7.0. Además, además, el script de instalación realiza modificaciones sobre el
fichero de composer.json" para adaptarlo a las versiones de los distintos módulos de Symfony2*
instalados.
12.3 Slim
Slim [38] es un micro-framework escrito en PHP, que según el propio proyecto, ayuda a desarrollar
rápidamente aplicaciones web y API.
Desarrollado por Josh Lockhart, autor del libro Modern PHP [5] de la popular página web "PHP The
Right Way".
La simplicidad the Slim como framework se puede ver con el clásico "hello world" que aparece como
ejemplo al comienzo de la documentación:
Para comenzar a explorar los tests de Slim necesitamos descargarnos el código fuente e instalar las
dependencias:
La versión de Slim sobre la que estamos estudiando los tests es la 2.6.2, aunque ya hay una versión
beta de Slim 3.
Como hicimos en el caso de Silex, para evaluar la calidad de los tests unitarios de Slim ejecutaremos
97
Capítulo 12. Test de proyectos existentes
Humbug running test suite to generate logs and code coverage data...
.............MMM.......E............E..M.................M.. | 60 ( 3/21)
.M..MM.............MSM...MMMM...........MM....M.M.MM....M.M. | 120 ( 5/21)
M.M........MMM.M..........M.MS..M.M..M...M.M..M.........M..M | 180 ( 6/21)
........M...........MM....................MMM....MMM........ | 240 ( 7/21)
.......M.............M.......M..M....................M...MMM | 300 (13/21)
..M.M.MSSSSS...M...MM.TEEEEEEEE.EE...M...MMS........M....... | 360 (16/21)
...E........SSS.M..M...M.M...M.M..SSS.....MME....E.........M | 420 (19/21)
MMSM.......
Metrics:
Mutation Score Indicator (MSI): 79%
Mutation Code Coverage: 97%
Covered Code MSI: 81%
Remember that some mutants will inevitably be harmless (i.e. false positives).
El valor de MSI de Slim es de 79% frente al 94% de la cobertura de tests unitarios, esto es un 15% de
diferencia. Esto nos indica que hay un conjunto de tests mayor que en Silex (proporcionalmente), que
no están testeando de la mejor forma el código, debido a que el test ha pasado después de que se han
realizado mutaciones en el código.
Al igual que Silex, Slim está integrado utilizando Travis CI. El contenido del fichero de configuración
.travis.yml para este proyecto es:
language: php
98
Capítulo 12. Test de proyectos existentes
php:
- 5.3
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm
Como podemos ver, la configuración es muy sencilla, teniendo en cuenta solo las versiones de PHP
para las que Slim es compatible (desde la 5.3 hasta la 7.0, incluyendo hhvm) y la generación de la
cobertura en formato texto.
99
Capítulo 13. Más Tests
La librería Reflexion contiene una completa interfaz de usuario por la que podemos realizar ingeniería
inversa a clases, interfaces, funciones, métodos y extensiones de nuestro propio código PHP.
En el siguiente ejemplo vamos a ver como crear un objeto con un constructor privado.
namespace Src;
/**
* Example class with private constructor.
*
*/
class ClassPrivateConstruct
{
/**
* Unique instance.
*
* @var \stdClass number of pings
*/
private static $instance = null;
/**
* Variable One.
*
* @var string
*/
protected $varOne = null;
/**
100
Capítulo 13. Más Tests
* Variable Two.
*
* @var string
*/
protected $varTwo = null;
/**
* Private constructor.
*
* @param string $varOne Variable One.
* @param string $varTwo Variable Two.
*
*
* @return void
*/
private function __construct($varOne, $varTwo)
{
$this->varOne = $varOne;
$this->varTwo = $varTwo;
}
/**
* Public creation of instance.
*
* @param string $varOne Variable One.
* @param string $varTwo Variable Two.
*
* @return ClassPrivateConstruct
*/
public static function create($varOne, $varTwo)
{
if (!self::$instance) {
self::$instance = new ClassPrivateConstruct($varOne, $varTwo);
}
return self::$instance;
}
}
A continuación, incluimos el código necesario para poder testear el fragmento de código anterior:
namespace Test;
use Src\ClassPrivateConstruct;
/**
* Testing for ClassPrivateConstruct Class
*
*/
class ClassPrivateConstructTest extends \PHPUnit_Framework_TestCase
{
/**
101
Capítulo 13. Más Tests
$propertyVarOne->setAccessible(true);
$propertyVarTwo->setAccessible(true);
$this->assertEquals("var1", $propertyVarOne->getValue($class));
$this->assertEquals("var2", $propertyVarTwo->getValue($class));
}
}
Como podemos ver, nuestra clase ClassPrivateConstruct lo único que hace es una posible
implementación de Singleton a la que le pasamos dos parámetros. En nuestro test, mediante la primera
aserción nos aseguramos de que el constructor es privado. En las dos siguientes aserciones, mediante
reflexión también, verificamos que los argumentos que le hemos pasado han sido correctamente
asignados en nuestra creación estática de nuestro objeto.
Para ilustrar una forma de testear generadores hemos creado la clase GeneratorClass, la cual tiene un
método getCountOfWords, que dado un fichero de texto devuelve de forma ordenada el número de
veces ese fichero contiene cada palabra.
Su implementación es:
namespace src;
/**
* Example of class that use generator.
*/
class GeneratorClass
{
/**
102
Capítulo 13. Más Tests
* Count of words.
*
* @var [string => int] Number of times that appear a word hashed by word.
*/
private $counts = [];
/**
* Get counts of words.
*
* @param string $file Filename.
*
* @return Generator
* @throws \Exception File not found.
*/
public function getCountOfWords($file)
{
$f = fopen($file, 'r');
if (!$f) {
throw new \Exception();
}
while ($line = fgets($f)) {
$parts = explode(' ', trim($line));
foreach ($parts as $word) {
if (!isset($this->counts[$word])) {
$this->counts[$word] = 1;
} else {
$this->counts[$word]++;
}
}
}
arsort($this->counts);
foreach ($this->counts as $word => $count) {
yield $word => $this->counts[$word];
}
fclose($f);
}
}
En nuestro test unitario, dado que queremos verificar que el uso de Generator, testeamos que el
método getCountOfWords pese a no tener ninguna sentencia return nos devuelve un objeto del tipo
Generator, que a su vez implementa el interfaz Iterator, como esperábamos. Además testeamos que el
primer valor devuelto por el conjunto de palabras asociado al número de veces es el esperado.
namespace Test;
use Src\GeneratorClass;
103
Capítulo 13. Más Tests
/**
* Testing for GeneratorClass
*/
class GeneratorClassTes extends \PHPUnit_Framework_TestCase {
/**
* Test for check a Generator
*
* @return void
*/
public function testGetLines() {
$generator = new GeneratorClass();
$dictionary = Array();
$counts = $generator->getCountOfWords("./tests/fixture/file.txt"
$this->assertEquals(4, $counts->current());
$this->assertEquals("word2", $counts->key());
$this->assertInstanceOf("Iterator", $counts);
$this->assertInstanceOf("Generator", $counts);
}
}
El contenido de nuestro fichero file.txt, el cual hace pasar este test es:
Testear comandos tiene una dificultad especial: El método público de su ejecución es el encargado de
realizar un gran conjunto de tareas sin devolver una salida testeable. Todas estas tareas que realiza el
método de ejecución, en un contexto de testeo unitario, deberíamos de crear los test dobles para
asegurarnos de que la parte testeada es la propia de la clase a testear.
Se considera buena práctica tener un log de ejecución de comando, para poder depurar errores o
mantener un registro de acciones. Además, utilizar un log en la ejecución de un comando nos da la
posibilidad de solucicionar la testeabilidad de comandos, pues podemos considerar nuestro log como
la salida del comando y verificar un resultado esperado.
Para ilustrar este problema y solución hemos creado la clase CommandClass: Esta clase es un
commando, sin pertenecer a ningún framework, que realiza un envío de mails a una lista pasada como
parámetro. El constructor de nuestro comando necesita como parámetros una instancia del objeto
Logger, que será el que en nuestro test inyectaremos con un log especial, y una instancia de una clase
Emailer, que también será mockeada en nuestro ejemplo.
104
Capítulo 13. Más Tests
namespace src;
/**
* CommandClass source code.
* Simulate a email sender command.
*/
class CommandClass
{
/**
* Logger.
*
* @var $logger
*/
private $logger = null;
/**
* Emailer.
*
* @var $emailer
*/
private $emailer = null;
/**
* Construct.
*
* @param \stdClass $logger Logger.
* @param \stdClass $emailer Emailer.
*
*/
public function __construct(\stdClass $logger, \stdClass $emailer)
{
$this->logger = $logger;
$this->emailer = $emailer;
}
/**
* Messages.
*
* @param array $messages Array of messages hashed as
* ["email"=> string, "content"=> string].
*
* @return void
*/
public function execute(array $messages)
{
$this->logger->log("Start command");
foreach ($messages as $message) {
$this->logger->log("Validating email ".$message['email']);
if (filter_var($message['email'], FILTER_VALIDATE_EMAIL)) {
$this->logger->log("Email ".$message['email']. " is valid", Lo
105
Capítulo 13. Más Tests
try {
$this->emailer->send($message['email'], $message['content'
$this->logger->log("Email ".$message['email']. " sended",
} catch (\Exception $e) {
$this->logger->log($e->getMessage(), Logger::ERROR);
}
} else {
$this->logger->log("Email ".$message['email']." is not valid",
}
}
$this->logger->log("End command");
}
/**
* Return $this->logger.
*
* @return \stdClass
*/
public function getLogger()
{
return $this->logger;
}
}
El test unitario de nuestra clase Command, como indicabamos anteriormente creará un objeto
CommandClass con una lista de emails de ejemplos y ejecutará nuestro método execute. Dado que
nuestro ejemplo es muy sencillo, no hemos creado mocks utilizando Prophecy como vimos en el
capítulo 6 (test dobles), sino que hemos creado dos clases en nuestro namespace de Test. Un Logger,
que deja en memoria los logs de forma que podamos recuperarlos para verificar su contenido, y un
Emailer, que siempre devuelve true como implementación del método send.
namespace Test;
use Src\CommandClass;
use Src\Logger;
use Src\Emailer;
/**
* Test for CommandClass
*/
class CommandClassTest extends \PHPUnit_Framework_TestCase
{
/**
* Test for a command class. A simple mail sender command.
*
* @return void
*/
public function testCommandSend()
106
Capítulo 13. Más Tests
{
$messages = [
["email" => "email@example.com", "content" => "Good example of mai
["email" => "emailexample.com", "content" => "Bad example of mail"
];
$logger = $command->getLogger();
$logs = $logger->getLogs();
$this->assertEquals($logs[0]['message'], 'Start command');
$this->assertEquals($logs[1]['message'], 'Validating email email@examp
$this->assertEquals($logs[2]['message'], 'Email email@example.com is v
$this->assertEquals($logs[3]['message'], 'Email email@example.com send
}
}
Para realizar estos tests PHPUnit [1] permite la creación de mocks. El siguiente ejemplo es extraído
de la documentación de PHPUnit y nos sirve para ilustrar esta situación:
$stub->expects($this->any())
->method('abstractMethod')
107
Capítulo 13. Más Tests
->will($this->returnValue(TRUE));
$this->assertTrue($stub->concreteMethod());
}
}
En este ejemplo podemos ver como PHPUnit tiene el método especial getMockForAbstractClass,
dedicado a crear mocks para clases abstractas y poder crear test unitarios.
De forma similar que PHPUnit soporta testeo para clases abstracta soporta testeo para traits, con la
necesidad de que debemos crear los mocks con el método getMockForTrait. El ejemplo siguiente,
también extraído de la documentación de PHPUnit nos enseña como testear este tipo de situaciones.
trait AbstractTrait
{
public function concreteMethod()
{
return $this->abstractMethod();
}
$mock->expects($this->any())
->method('abstractMethod')
->will($this->returnValue(TRUE));
$this->assertTrue($mock->concreteMethod());
}
}
información a la vista con el objeto de presentarle el resultados del envío de comandos al modelo en
la propia vista.
Al ser una parte tan importante del desarrollo web, testear estos controladores en cada framework
tiene una implementación particular. En este PFC no expondremos cada una de las formas de testear
un controlador, dado que cada framework tiene una documentación específica para poder ser testeado.
Para ver más detalles sobre la forma de testear este tipo de clases, recomendamos consultar las
siguiente lista:
Información de como testear en Laravel 5.1 [40]. Este documento no está centrado solo a cómo
testear controladores, aunque en la sección Application Testing queda reflejada como
extendiendo la clase TestCase de Laravel se pueden testear este tipo de clases.
Información de como testear en Symfony2 [41]. En esta página se detalla como testear en
Symfony2. En el apartado "Functional Tests" detallan como se pueden crear test que
extendiendo de la clase propia de Symfony2, WebTestCase, se pueden testear controladores.
Información de como testear en Zend2 [42]. En el apartado Your first controller tests explican
como testear controladores en Zend2.
Además, como vimos en el capítulo 9, Codeception soporta test funcionales para estos frameworks y
otros como Yii2 y Phalcon, como test funcionales.
Mockearlas
Imaginemos que tenemos un trozo de código donde llamamos la función nativa file_get_contents, que
lo que hace es obtener el contenido de un fichero o url. Para poder testear esta función podemos
encapsularla dentro de otra de la siguiente forma:
class SomeClass
109
Capítulo 13. Más Tests
Alojarlas en un namespace
Otra solución sería utilizar namespaces. Agrupando una colección de funciones dentro de un
namespace nos ayuda a mockear cuando testeemos código que dependa de estas funciones. Un
ejemplo de esta situación sería el siguiente:
namespace App\Helpers;
function showTime() {
return time();
}
De esta forma estamos redefiniendo la función time para el namespaces App, pudiendo ser utilizada
en un tests unitario de la siguiente forma:
namespace App\Helpers;
Como vemos, hemos añadido una definición de la función time en nuestro código del test, lo que hará
que por defecto, cuando la llamada de la función showTime es ejecutada busca una función time
dentro del mismo namespace y si la encuentra la ejecuta. Realmente no es una forma de mockear,
pero es una forma de alterar el comportamiento de funciones de PHP para mejorar la testeabilidad de
nuestro código.
110
Capítulo 13. Más Tests
Personalmente creo que tener una cobertura de tests de 90% o 100% que no aportan valor es algo que
carece de importancia. Si pensamos que en algunos objetos los getters y setters pueden ser generados,
como por ejemplo en las entidades generadas por el ORM Doctrine2, se podría incluso auto generar
los tests unitarios de estos getters y setters. Sin embargo, para este tipo de métodos que tengan algún
tipo de lógica implementada tiene sentido que esta lógica quede descrita mediante uno o varios tests.
Además de este mecanismo, y como podemos ver en la documentación de PHPUnit [1] es sencillo
extender las distintas partes del framework. Por ejemplo, podemos extender PHPUnit para mostrar
qué test ha sido lento, como hace el proyecto phpunit-speedtrap [43]. Este proyecto crea Listeners que
deben ser configurados en el fichero "phpunit.xml" o "phpunit.xml.dist" con el tiempo que se
considera a ser remarcado, y el número de tests lentos que se quieren presentar.
111
Conclusiones
Conclusiones
Conclusiones
En este PFC hemos intentado dar visibilidad a las distintas herramientas para la creación de tests
automáticos y las distintas metodologías que podemos realizar. Para ello, hemos intentado dar
respuesta a los planteamientos que planteábamos en el anteproyecto, basándonos en cinco objetivos:
Estudiar como testar una clase en PHP. Para dar respuesta a este apartado hemos creado una
clase que mejoraba el tipo de datos string de PHP y hemos creado test unitarios en dos
frameworks de testeo, PHPUnit y Atoum. En la memoria de este PFC hemos expuesto algunos
ejemplos de los tests, intentando dar visibilidad a las diferencias estas dos herramientas, aunque
demostrando que ambas son válidas para el objetivo de este punto.
Testo de bases de datos. En el capítulo 5 hemos explicado los distintos puntos que debemos
considerar cuando planteamos realizar tests a un desarrollo con dependencias con bases de
datos. Hemos planteado un ejemplo en el que el código testeado realizaba consultas a bases de
datos, mostrando hincapié en la complejidad que este tipo de test de integración (hay que
destacar que no sse pueden considerar test unitarios) debido a la dificultad para crear un entorno
bajo control, similar a producción.
En el capítulo 6 hemos trabajado en dar respuesta al objetivo del uso de mocks, creando una
extensión de Monolog para enviar mensajes logs a Apache Solr. En el desarrollo de este
objetivo vimos que existía una gran cantidad de librerias para gestionar mocks por lo que
decidimos profundizar en este punto, con la misión de exponer la mejor opción para cada
situación. Respecto a todas estas librerías hemos llegado a una conclusión: Siempre que sea
posible, es preferible utilizar Prophecy frente a las otras alternativas. Llegamos a la conclusión
puesto que es la herramienta más estricta, lo cual nos exigirá implementar nuestro código
siguiendo buenas prácticas, frente a atajos que nos facilitan herramientas. Consideramos como
herramientas de mocks poco recomendables las que permiten utilizar mocks parciales, mocks de
clases no existentes, etc.
En nuestro anteproyecto pusimos como objetivo testear con Selenium una aplicación que pueda
existir en producción. Este objetivo lo hemos cumplimentado en los capítulos 8 y 9, aunque no
hemos usado directamente Selenium, sino que hemos usado otras herramientas más modernas,
que a su vez, utilizar Selenium u otros simuladores de navegadores. En el capítulo 8 testamos
algunas funcionalidades básicas de WordPress utilizando Behat y en el capítulo 9 repetimos el
ejercicio con Codeception, con el objetivo de estudiar distintas alternativas. Debido a las
grandes diferencias que existen entre estas dos herramientas, no podemos concluir sobre el uso
de una frente a la otra. Ambas herramientas ofrecen ventajas e inconvenientes, no siendo
excluyentes. En nuestra opinión, algunas partes de un proyecto podrían estar testeadas
utilizando Behat y otras utilizando Codeception. De esta forma se podrían obtener las ventajas
112
Conclusiones
Tras la realización de este proyecto podemos concluir que trabajar con generación de test ofrece una
gran cantidad de ventajas frente a no hacerlo. Algunas de estas ventajas son cuantificables a través de
las herramientas de análisis estático de código. Podemos llegar a esta conclusión debido a algunas
prácticas que hay que mantener cuando escribimos código testeable:
Todos los puntos mencionados anteriormente pueden ser alcanzados sin testeo automático, pero no se
puede realizar testeo si el código no cumple estos puntos, por lo que por necesidad, un código con
testeo automático deberá ser mejor que uno sin testeo.
Las ventajas mencionadas anteriormente justifican solo mejoras a nivel de calidad de código, pero no
hay que olvidar que el testeo automático ofrece otras ventajas, como hemos visto a lo largo de este
PFC, como son regresión, menor índice de errores,
Herramientas utilizadas
Para realizar este PFC hemos realizado:
Vim, como editor de código PHP para los distintos ejemplos y Gedit como editor de textos para
la memoria, en formato Markdown
En este documento hemos intentado incluir herramientas mantenidas por la comunidad y hemos
dejado de lado otras abandonadas como pueden ser SimpleTest, SnapTest, etc. Una revisión de
estas herramientas nos podría haber enseñado la forma en la que se trabaja anteriormente.
En las próximas fechas a la edición de este documento saldrá a la luz la versión PHP 7. En este
documento no hemos abordado los posibles cambios del lenguaje con respecto a la forma en la
que testeamos, cuando pensamos que algunos cambios de PHP 7 cambiarán la forma de testear.
113
Conclusiones
Por ejemplo, uno de los cambios que promete PHP 7 y que merece la pena destacar aquí por el
impacto que tendrá en la forma que testeamos en PHP es el tipado estricto. Hasta el momento,
verificar en un test que la devolución de un método o función tenía un tipo concreto era
habitual, dado que PHP 5 y versiones anteriores es de tipado dinámico. A partir de PHP 7 el
tipo de datos de la devolución de una función puede ser definido en la función, por lo que no
será necesrario testearlo más. Esto puede que afecte a la forma en la que usamos las
herramientas de testing.
Con este cambio de versión, todos los frameworks que hemos analizado en este documento
deberían de evolucionar para adaptarse al lenguaje. Algunos de estas herramientas tienen
prevista una fecha por la lanzar la versión correspondiente a PHP 7, como PHPUnit, que en
diciembre de 2016 prevé tener la versión 6.0, que dará soporte únicamente a PHP 7.
Otra linea de investigación, aunque no estrictamente relacionada con el testing, pero sí con la
calidad de código sería la relacionada con las herramientas de análisis estático de código,
métricas de calidad de código, etc. Estas herramientas, al igual que la práctica de crear tests,
tiene un objetivo común con el testing: mejorar la calidad del software y minimizar los errores.
Produndizar en estas herramientas es valioso para cualquier lenguaje de programación.
En este PFC nos hemos centrado en un único lenguaje de programación: PHP. Sin embargo, hay
multitud de lenguajes. Este estudio podría realizarse a casi cualquier lenguaje de programación,
desde Java hasta Erlang. Estudiar como crear test automáticos para las herramientas que
usamos, desde el punto de vista de desarrollador de software, es una tarea tremendamente útil y
en algunos sectores del mundo laboral, imprescindible.
Además, no podemos sabemos qué ideas surgirán de la comunidad de desarrollo, o que práctica será
importada de otra comunidad. Estudiar y aprender sobre lenguajes de programación, o sobre partes
tan pequeñas como el testeo automático, puede ser extendido y profundizado extensamente.
114
Bibliografía
Bibliografía
Bibliografía
[1] Sebastian Beerman, documentación de PHPUnit. Documento en formato HTML accesible por
internet en la dirección: http://phpunit.de.
[2] Varios autores, "PHP Framework Interop Group. Documento en formato HTML accesible por
internet en la dirección: http://www.php-fig.org.
[3] Zdenek Machek, "PHPUnit Essentials", Packt Publishing, 2014. ISBN: 978-1-78328-343-9.
[5] Josh Lockhart, "Modern PHP: New Features and Good Practices", O'Reilly, 2015. ISBN: 978-1-
491-90501-2.
[6] Varios autores, documentación de Prophecy. Documento en formato HTML accesible por internet
en la dirección: https://github.com/phpspec/prophecy.
[7] Varios autores, documentación de Atoum. Documento en formato HTML accesible por internet en
la dirección: https://github.com/atoum/atoum
[8] Michael Feathers, "Working Effectively with Legacy Code", Prentice Hall, 2004. ISBN-13: 007-
6092025986
[9] Varios autores. Artículo de Wikipedia (en inglés) "Software testing". Documento en formato
HTML accesible por internet en la dirección:
https://en.wikipedia.org/wiki/Software_testing#Testing_Types
[10] Página oficial de Github.com. Sitio web completo accesible por internet en la dirección:
https://github.com.
[11] Varios autores, documentación de Composer. Sitio web completo accesible por internet en la
dirección: https://getcomposer.org/doc/.
[12] Página oficial de Packagist. Sitio web completo accesible por internet en la dirección:
https://packagist.org
[13] Varios autores, documentación de Faker. Documentación en formato HTML accesible por
internet en la dirección: https://github.com/fzaninotto/Faker.
[14] Varios autores, documentación de Alice. Documentación en formato HTML accesible por
internet en la dirección: https://github.com/nelmio/alice.
[15] Varios autores, documentación de Samsui. Documentación en formato HTML accesible por
internet en la dirección: https://github.com/mauris/samsui.
[17] Documentación de Phake. Documento en formato HTML accesible por internet en la dirección:
115
Bibliografía
http://phake.readthedocs.org/en/.
[19] Martin Fowler, artículo "Mocks Aren't Stubs". Documento en formato HTML accesible por
internet en la dirección: http://martinfowler.com/articles/mocksArentStubs.html
[20] Konstantin Kudryashov, "Conceptual difference between Mockery and Prophecy". Documento
en formato HTML accesible por internet en la dirección:
http://everzet.com/post/72910908762/conceptual-difference-between-mockery-and-prophecy.
[27] Página oficial de Codeception. Documentación en formato HTML accesible por internet en la
dirección: http://codeception.com/.
[28] API Documentation of Flickr.com. Documentación en formato HTML accesible por internet en la
dirección: https://www.flickr.com/services/api/.
[29] Documentación de Oauth 1.0. Documentación en formato HTML accesible por internet en la
dirección: http://oauth.net/core/1.0/.
[33] Varios autores. Artículo de Wikipedia (en inglés) "Comparison of continuous integration
software". Documento en formato HTML accesible por internet en la dirección:
https://en.wikipedia.org/wiki/Comparison_of_continuous_integration_software
[35] Página oficial de Travis CI. Sitio web completo accesible por internet en la dirección:
116
Bibliografía
https://travis-ci.org/.
[39] Documentación sobre Generators. Documentación en formato HTML accesible por internet en la
dirección: http://php.net/manual/en/language.generators.overview.php.
[40] Documentación de testeo para Lavarel 5.1. Documentación en formato HTML accesible por
internet en la dirección: http://laravel.com/docs/5.1/testing.
[41] Documentación de testeo para la última versión de Symfony. Documentación en formato HTML
accesible por internet en la dirección: http://symfony.com/doc/current/book/testing.html.
[42] Documentación de testeo Zend2. Documentación en formato HTML accesible por internet en la
dirección: http://framework.zend.com/manual/2.0/en/user-guide/unit-testing.html.
[44] Varios autores. Artículo de Wikipedia (en inglés) "Agile software development". Documento en
formato HTML accesible por internet en la dirección:
https://en.wikipedia.org/wiki/Agile_software_development
[45] Sitio oficial de calidad de código PHP, "The PHP Quality Assurance Toolchain". Documento en
formato HTML accesible por internet en la dirección: http://phpqatools.org/
[46] Sebastian Beerman, sitio web "Jenkins PHP". Documento en formato HTML accesible por
internet en la dirección: http://jenkins-php.org.
[47] Artículo sobre PageObject Pattern. Documento en formato HTML accesible por internet en la
dirección: https://code.google.com/p/selenium/wiki/PageObjects.
[48] Gerard Meszaros, "Xunit Test Patterns: Refactoring Test Code", Addison-Wesley Professional,
2007. ISBN-13: 007-6092037590.
117