Hace unos días vimos algunos patrones para mejorar los tests con PHPUnit, mejorando los assert y/o fixtures. En esta ocasión traemos una serie de patrones para mejorar nuestros tests en PHP.
Veremos una serie de técnicas sobre como afrontar los tests, haremos ejemplos, si bien es cierto que estos ejemplos serán en PHP, la teoría detrás de estos tests puede aplicarse a Java (JUnit), .NET, Python,…
Parametrized test
De vez en cuando, nos encontramos con que estamos escribiendo pruebas casi idénticas, que solo difieren en unos pocos valores, pero la lógica es esencialmente la misma. En estas situaciones es una buena práctica a la hora de hacer test utilizar el patron test parametrizados (parametrized test).
La idea fundamental es de este patrón reside en que solo hay un método de prueba que encapsula la lógica, con la ayuda de PHPUnit lo que hacemos es proporcionar a ese test distintos conjuntos de parámetros.
Veamos un ejemplo del patrón de test patametrized test
/**
* @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, 2),
);
}
Utilizando la notación @dataProvider
le decimos a PHPUnit que este tes utilizará los datos devueltos por la función additionProvider
. Este patrón puede servirnos como guía para soluciones un poco más elaboradas, por ejemplo podemos leer de un fichero los valores que queremos probar de esta manera
public function additionProvider()
{
return new CsvFileIterator('data.csv');
}
Utilizando este patrón ganamos mucha legibilidad en los tests, algo que es de agradecer cuando pasado un tiempo tenemos que volver a leer el código y los tests que escribimos.
Self-Shunt
Muchas veces tenemos objetos que interactúan con muchos otros a su alrededor, es decir tienen muchas dependencias y a veces se nos hace muy costoso testear un método que necesita un objeto que es bastante difícil de crear. El patrón Self-Shunt se usa cuando utilizar un Mock no es viable, o la clase que estamos testeando tiene métodos no visibles (protected o private) que nos interesa utilizar.
Con un ejemplo del patrón quizás lo veamos más claro.
class TestSpyTest extends PHPUnit_Framework_TestCase implements Mailer, Db
{
public function testSendsAMailAfterUserCreationViaSelfShunting()
{
// remember, Db and Mailer would be two different objects in production
$sut = new UserDao($this, $this);
$sut->createUser(array('mail' => 'someone@example.com', 'nickname' => 'johndoe'));
$this->assertEquals(array('executeQuery', 'mail'), $this->order);
}
private $order = array();
private $queries = array();
public function executeQuery($query, array $params)
{
$this->order[] = 'executeQuery';
$this->queries[] = $query;
}
private $mails = array();
public function mail($to, $subject, $object)
{
$this->order[] = 'mail';
$this->mails[] = array('to' => $to,
'subject' => $subject,
'object' => $object);
}
}
Imaginemos que queremos testear la creación de usuarios, clase UserDao, esta clase envía un email y hace una inserción en BD cada vez que se crea un usuario. Hasta aquí el comportamiento normal, pero insertar y mandar un email cada vez que ejecutamos un test es algo un poco «chungo», solo queremos testear que eso ocurre, ya nos encargaremos en otros test de testear el envío e emails o la inserción en BD.
Para ello lo que hacemos es sobrescribir los métodos de la interfaz Db y Mailer, en el propio test, con ello «flaseamos» los métodos de enviar email y de inserción en BD para solo probar una cosa cada vez, con esto ya podemos hacer testing de la creación de usuarios.
El patrón self-hunt es una excelente herramienta para implementar test dobles de una manera sencilla.
Test-Specific Subclass
Todos los objetos tienen algún estado privado, es decer clases que tienen variables protected a las que solo se accede mediante métodos getter y setter. Hay veces que necesitamos modificar este estado privado para poder testear SUT y no disponemos de estos métodos getter y setter o los métodos son protected.
Con la ayuda del patrón Test-Specific Subclass podemos sobrescribir los métodos de la clase original para facilitar el test.
El patrón Test-Specific Subclass se utiliza cuando por ejemplo queremos testear un método privado o protected dentro de nuestro test. También podemos utilizar este patrón sustituir al patrón Singleton, o incluso cuando no queremos utilizar los Mocks de PHPUnit. Quizás con un ejemplo lo veamos todo un poco más claro.
Esta es la clase que queremos testear:
class MoneyFund
{
public function __construct($capital, $interestRate)
{
$this->capital = $capital;
$this->interestRate = $interestRate;
}
public function afterYears($years)
{
for ($i = 1; $i <= $years; $i++) {
$this->capital += $this->calculateInterests();
}
}
protected function calculateInterests()
{
return round($this->capital * $this->interestRate / 100, 2);
}
public function getCapital()
{
return $this->capital;
}
}
Como podemos ver el método calculateInterests no podemos testearlo ya que es protected, si fuese private ni siquiera podríamos testearlo, aunque la visibilidad de los métodos es un tema que incluso daría para un post, la regla general es “utiliza protected siempre que no tengas una buena excusa para utilizar private”
Para poder testar el método calculateInterest lo que vamos a hacer es crear una clase interna que extienda a la clase *MoneyFind”, en ella crearemos un método public para utilizar “caculateInterest”
class TestSpecificSubclassTest extends PHPUnit_Framework_TestCase
{
public function testPrivateMethodOfMoneyFund()
{
$moneyFund = new TestSpecificMoneyFund(100000, 4);
$this->assertEquals(4000, $moneyFund->calculateInterests());
}
}
class TestSpecificMoneyFund extends MoneyFund
{
public function calculateInterests()
{
return parent::calculateInterests();
}
}
Listo con esto ya podemos tesear los métodos protected.
Exception Testing
A veces nos encontramos métodos que lanzan excepciones, también necesitamos testar estos casos y la mejor manera de hacerlo es apoyarnos en PHPUnit. PHPUnit ofrece la notación @expectedException, en la documentación de PHPUnit podemos ver las distintas maneras de utilizarlo, a mí particularmente me gusta utilizar la notación con @. Veamos un ejemplo.
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException MyException
* @expectedExceptionCode 20
*/
public function testExceptionHasErrorcode20()
{
throw new MyException('Some Message', 20);
}
}
Database Sandbox
Siempre que ejecutamos test que tienen que interactuar con la BD nos echamos a temblar, ya que dependemos muchos de los datos que estén guardados en la BD. Una podría ser tener una BD solo para las pruebas, pero si un test suite no termina o falla tendríamos datos inconsistentes, ¿que podemos hacer?
Una solución podría ser utilizar un esquema y datos para cada test, como vamos a ver en el ejemplo creamos una pequeña BD en memoria que utilizamos para el test.
class DatabaseSandboxTest extends PHPUnit_Framework_TestCase
{
protected $db;
public function setUp()
{
$this->db = new PDO('sqlite::memory:');
$this->db->exec('CREATE TABLE users (id INT NOT NULL PRIMARY KEY, name VARCHAR(255))');
$this->db->exec('INSERT INTO users (id, name) VALUES (1, "Juan")');
}
public function testMyDatabaseIsNewShinyAndPopulatedWithData()
{
$this->assertTrue($this->db instanceof PDO);
$users = $this->db->query('SELECT * FROM users')->fetchAll();
$this->assertEquals('Giorgio', $users[0]['name']);
}
}
Conclusiones
Hemos visto una serie de patrones que nos ayudan cuando estamos haciendo tests, ya sea como TDD, trabajando con Legacy code testeando nuestro código. Existen muchos más patrones y muchas mejores formas de testar código, estos solo una pequeña parte.
¿Y vosotros tenéis triquiñuelas para probar algunas partes del código o algunos patrones? ¿Cómo os enfrentáis a los tests cuando tenéis que testear alguna clase?
Un comentario en “patrones test utilizando PHPUnit”