Más sobre tests
En esta sección estudiaremos distintas posibilidades no contemplados hasta el momento sobre testeo. Por ejemplo, veremos como testear algunas de las últimas funcionalidades añadidas por el lenguaje PHP como son los Generators y Traits. También veremos cómo testear algunas situaciones no triviales de testear como son los métodos constructores privados, la ejecución de comandos, controladores y funciones globales. Además plantearemos algunos debates que suelen surgir en los distintos equipos de desarrollos como la cuestión si merece la pena testear los métodos setters y getters, así como mencionaremos cómo extender PHPUnit para conseguir saber qué tests son lentos.
13.1 Testeando constructor privado
Una situación especial que nos podemos encontrar, y que realizar un test unitario no es sencillo, es testear un constructor privado. Este tipo de constructores son utilizados en algunos patrones de diseño, como pueden ser Singleton. En estas situaciones especiales podemos tener alguna lógica en el constructor que merezca la pena testear, pero al ser un método privado, testearla desde un test unitario directamente no es algo trivial. Para conseguir el test deseado debemos de utilizar reflexión mediante la librería de PHP Reflexion.
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;
/**
* 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
{
/**
* Test for private constructor.a
*
* @return void
*/
public function testConstruct()
{
$reflectionClass = new \ReflectionClass(ClassPrivateConstruct::class);
$meth = $reflectionClass->getMethod('__construct');
$this->assertTrue($meth->isPrivate());
$class = ClassPrivateConstruct::create("var1", "var2");
$propertyVarOne = $reflectionClass->getProperty('varOne');
$propertyVarTwo = $reflectionClass->getProperty('varTwo');
$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.
13.2 Testeando generadores
En la versión 5.5 de PHP se incluyó el concepto de Generator. Un Generator no es más que un mecanismo para implementar de forma sencilla el patrón de diseño Iteraror, sin la sobrecarga de trabajo de implementar la lógica de dicho patrón. La documentación oficial de PHP da más información sobre esta nueva herramienta del lenguaje [39]. En el capítulo 2 de Modern PHP [5] se explica con detalle los Generators con algunos ejemplos de uso.
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
{
/**
* 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.
La implementación de nuestro test unitario sería:
namespace Test;
use Src\GeneratorClass;
/**
* 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:
word1 word2 word3 word4
word2 word2 word2 word3
word3 word4 word3 word5
13.3 Testeando comandos
En muchos desarrollos es necesario crear comandos: Un comando es una ejecución de un script, generalmente en línea de comandos (no en un entorno web) que realiza tareas que deben ser realizadas en segundo plano. Normalmente un comando tiene un único método público para su ejecución (llamados run, execute, process, ...) y es llamado desde el una interfaz definida para ello. Los frameworks modernos de PHP como Symfony2 y Lavarel tienen distintos interfaces para implementar y ejecutar comandos, aunque el concepto de base es muy similar.
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.
El código de nuestra clase CommandClass sería el siguiente:
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", Logger::DEBUG);
try {
$this->emailer->send($message['email'], $message['content']);
$this->logger->log("Email ".$message['email']. " sended", Logger::DEBUG);
} catch (\Exception $e) {
$this->logger->log($e->getMessage(), Logger::ERROR);
}
} else {
$this->logger->log("Email ".$message['email']." is not valid", Logger::DEBUG);
}
}
$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.
La implementación de nuestro test unitario sería:
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()
{
$messages = [
["email" => "[email protected]", "content" => "Good example of mail"],
["email" => "emailexample.com", "content" => "Bad example of mail"],
];
$command = new CommandClass(new Logger(), new Emailer());
$command->execute($messages);
$logger = $command->getLogger();
$logs = $logger->getLogs();
$this->assertEquals($logs[0]['message'], 'Start command');
$this->assertEquals($logs[1]['message'], 'Validating email [email protected]');
$this->assertEquals($logs[2]['message'], 'Email [email protected] is valid');
$this->assertEquals($logs[3]['message'], 'Email [email protected] sended');
$this->assertEquals($logs[4]['message'], 'Validating email emailexample.com');
$this->assertEquals($logs[5]['message'], 'Email emailexample.com is not valid');
$this->assertEquals($logs[6]['message'], 'End command');
}
}
13.4 Testeando clases abstractas
Desde el punto de vista de testeo unitario, testear clases abstractas no tiene sentido excepto en los métodos implementados en la clase abstracta. No es tan poco común que en el desarrollo de una librería, tengamos la necesidad de crear clases abstractas con el objetivo de extenderlas en las distintas capas de la aplicación. Para estar seguro de que estas implementaciones estén testeadas es necesario crear test unitarios para estas clases abstractas.
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:
abstract class AbstractClass
{
public function concreteMethod()
{
return $this->abstractMethod();
}
public abstract function abstractMethod();
}
class AbstractClassTest extends PHPUnit_Framework_TestCase
{
public function testConcreteMethod()
{
$stub = $this->getMockForAbstractClass('AbstractClass');
$stub->expects($this->any())
->method('abstractMethod')
->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.
13.5 Testeando traits
Los traits fueron incluidos en la versión 5.4 de PHP. En el capítulo 2 del libro "Modern PHP" [5] explican con detalle la forma de utilizar esta nueva funcionalidad de PHP.
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();
}
public abstract function abstractMethod();
}
class TraitClassTest extends PHPUnit_Framework_TestCase
{
public function testConcreteMethod()
{
$mock = $this->getMockForTrait('AbstractTrait');
$mock->expects($this->any())
->method('abstractMethod')
->will($this->returnValue(TRUE));
$this->assertTrue($mock->concreteMethod());
}
}
En el ejemplo creamos un mock de la clase AbstractClass, mockeamos el comportamiento del método abstracto y verificamos el comportamiento del método concreto, tal y como deberíamos hacer para verificar la funcionalidad de la clase abstracta.
13.6 Testeando controladores
En los frameworks PHP orientados a web, como pueden ser Symfony2, Zend Framework o Laravel existen, aunque de distintas formas, los métodos controladores, que dentro de la arquitectura MVC (patrón de diseño formado por el modelo, la vista y el controlador) son los responsables de enviar comandos al modelo para actualizar el estado del modelo. También es el responsable de enviar 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.
13.7 Testeando funciones globales
Cuando nos encontramos con funciones globales podemos estar en situaciones en la que la testeabilidad sea complicada. Algunas de estas situaciones las encontramos con funciones con dependencias externas. Por ejemplo funciones como file_get_contents o time. Frente a estas situaciones tenemos dos alternativas:
- 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
public function fetch($file)
{
return file_get_contents($file);
}
public function getContents($file) {
return "Content for $file:".$this->fetch($file);
}
}
Testear este método podría realizarse de la siguiente forma:
class SomeClassTest extends PHPUnit_Framework_TestCase {
public function testGetContents()
{
$mock = Mockery::mock('SomeClass')->makePartial();
$mock->shouldReceive('fetch')
->once()
->with('foo')
->andReturn('bar');
$sentence = $mock->getContents('foo');
$this->assertEquals('Content for foo: bar', $sentence);
}
}
- 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;
function time() { return 'foo'; }
class FunctionsTest extends PHPUnit_Framework_TestCase {
public function testShowTime()
{
$this->assertEquals('foo', showTime());
}
}
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.
13.8 Testeando getters y setters
Un debate relativo al testeo de un proyecto es el de testear los getters y setters de las distintas clases, lo cual divide a la comunidad de desarrolladores. En la sección Frequently Asked Questions del libro Lavarel testing decode [4] también es comentado.
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.
13.9 Extendiendo PHPUnit
PHPUnit puede ser extendido de varias formas. La más común es, como hemos visto en este capítulo, crear una clase que extienda de PHPUnit_Framework_TestCase y contenga nuevos métodos o sobreescriba otros según necesidades específicas de la situación en la que nos encontremos. Esto lo hemos visto en las formas de testear de Symfony2 y su clase WebTestCase, y en Laravel, y su clase TestCase.
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.
Este plugin se basa en una clase que implementa el interfaz PHPUnit_Framework_TestListener de PHPUnit y crea los métodos necesarios para registrar el tiempo de ejecución de cada tests.