Aislamiento de tests e interacción con datos
4.1 Que son fixtures y como se configuran
Una de las tareas que consumen más tiempo a la hora de crear tests es escribir el código que configura el sistema en un estado conocido. Este estado conocido se le llama fixture.
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.
4.1.1 Métodos setUp y tearDown
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.
class QueueTest extends PHPUnit_Framework_TestCase {
public function setUp(){
$this->queue = Array();
}
public function tearDown(){
unset($this->queue);
}
public function testEmptyQuery() {
$this->assertTrue(empty($this->queue));
}
public function testPush() {
array_push($this->queue, 'element1');
$this->assertEquals('element1', $this->queue[count($this->queue)-1]);
$this->assertFalse(empty($this->queue));
}
}
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 refactorizamos el ejemplo del apartado anterior con el método setUp y tearDown podría quedar de la siguiente forma:
class XStringsTest extends PHPUnit_Framework_TestCase{
protected $xstring = null;
public function setUp(){
$this->xstring = new XString('hello world');
}
public function tearDown(){
unset($this->xstring);
}
/**
* 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'));
}
}
4.1.2 Métodos setUpBeforeClass y tearDownAfterClass
Equivalentes a setUp y tearDown existen los métodos setUpBeforeClass y tearDownAfterClass. Estos métodos, en lugar de ejecutarse antes y después de cada tests, son ejecutados antes y después de la ejecución de la clase. El objetivo de estos métodos es inicializar y limpiar variables, respectivamente, de entorno que son reutilizadas entre los tests de una misma clase de testeo. Un ejemplo clásico de este tipo de asignación es el de la conexión a una base de datos. Dado que inicializar una conexión es costoso, realizar este tipo de inicializaciones una vez en la ejecución de la clase de test mejora el tiempo en de ejecución de los test, lo cual puede llegar a ser un problema cuando llegan a durar mucho tiempo.
El siguiente ejemplo nos mostraría como realizar dicha inicialización y limpiado:
class DatabaseTest extends PHPUnit_Framework_TestCase
{
protected static $dbh;
public static function setUpBeforeClass()
{
self::$dbh = new PDO('sqlite::memory:');
}
public static function tearDownAfterClass()
{
self::$dbh = NULL;
}
}
4.1.3 Anotaciones de PHPUnit
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:
class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
*/
public function testException()
{
}
}
El test testException pasará si el SUT que es ejecutando lanza una excepción del tipo InvalidArgumentException, en caso el test devolverá un error.
4.1.4 Estados globales
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.
Para testar el patrón Singleton se recomienda generalmente el uso de inyección de dependencias, como veremos en próximos capítulos.
En PHP, las variables globales son aquellas propias del entorno y son: GLOBALS, _ENV, _POST, _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.
4.1.5 Anotación @dataProvider
Un método de testeo puede aceptar parámetros arbitrarios.
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:
class DataTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected) {
$this->assertEquals($expected, $a + $b);
}
public function additionProvider() {
return array(
array(0, 0, 0),
array(0, 1, 1),
array(1, 0, 1),
array(1, 1, 3)
);
}
}
4.2 Herramienta de generacion de fixtures automáticas
Dado que la generación de fixtures es un trabajo que puede llegar a ser bastante tedioso y generalmente el valor de los datos no es tan importante como validar que el código haga lo esperado, existen algunas herramientas para ayudarnos a generar esta información.
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 ficticias con forma de direcciones americanas.
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;
protected $reviews = Array();
public function __construct($username, $email) {
$this->username = $username;
$this->email = $username;
}
public function addReview(Review $review) {
$this->reviews[] = $review;
}
public function countReviews() {
return count($this->reviews);
}
}
class Review {
protected $title;
protected $description;
public function __construct($title, $description) {
$this->title = $title;
$this->description = $description;
}
}
El código que testararía la clase User sería:
class UserTest extends \PHPUnit_Framework_TestCase{
public function setUp(){
$faker = Faker\Factory::create();
$faker->addProvider(new Internet($faker));
$this->user = new User($faker->userName, $faker->email);
}
public function testAddReview() {
$faker->addProvider(new Lorem($faker));
for($i=0;$i<=10;$i++) {
$this->user->addReview(new Review($faker->sentence, $faker->paragraph));
}
$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()>
Y el siguiente código PHP:
public function testAddReview() {
$loader = new Nelmio\Alice\Loader\Yaml();
$reviews = $loader->load(__DIR__.'/fixtures/reviews.yml');
foreach($reviews as $review) {
$this->user->addReview($review);
}
$this->assertEquals(10, $this->user->countReviews());
}
Mediante el fichero "fixture/review.yml" estamos especificando un rango de objetos en la linea 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.
Un ejemplo de código con Samsui sería el siguiente:
use Samsui\Factory;
use Samsui\Generator\Generator;
$factory = new Factory();
// define an object quickly
$factory->define('person')
->sequence('personId')
->attr('firstName', Generator::person()->firstName)
->attr('lastName', Generator::person()->lastName)
->attr('email', function ($i, $o) {
return Generator::email()->emailAddress(
array(
'firstName' => $o->firstName,
'lastName' => $o->lastName,
'domains' => array(
'hotmail.com',
'gmail.com',
'example.com'
)
)
);
})
->attr('createdTime', function () {
return time();
});
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.
4.2.4 Conclusión entre Faker, Alice y Samsui
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.