Todos alguna vez hemos trabajado en aplicaciones legacy, sin test, que utilizan muchos o muchísimos métodos estáticos, o se instancian varios objetos dentro de métodos sin razón aparente. Esto hace que sea un castigo testar estas aplicaciones para aplicar un cambio y dormir tranquilos, pero queramos o no esto es parte de nuestro trabajo. A veces es posible refactorizar el código e inyectar dependencias, en cambio muchas otras veces el esfuerzo de refactorizar es tanto que no podemos permitírnoslo ya sea porque no tenemos suficientes test y hacerlos es costoso o porque no tenemos tanto tiempo como querríamos para hacerlo.
Buscando información sobre como afrontar estos retos hace poco leí un artículo en el que se utilizaba Mockery y PhpUnit para intentar solucionar el siguiente problema.
Supongamos que tenemos una clase que modela a un Usuario, algo más o menos así:
class User
{
protected $address;
protected $addressId;
public function setAddressId($addressId)
{
$this->addressId = $addressId;
return $this;
}
public function getAddressId()
{
return $this->addressId;
}
public function getAddress()
{
if ($this->getAddressId() === null) {
return null;
}
if ($this->address === null) {
$this->address = new Address($this->getAddressId());
}
return $this->address;
}
}
Para testear esta clase, podemos hacer algo como lo siguiente:
class UserTest extends PHPUnit_Framework_TestCase
{
public function testGetSetAddressId()
{
$user = new User();
$this->assertNull($user->getAddressId());
$this->assertSame($user, $user->setAddressId(123));
$this->assertEquals(123, $user->getAddressId());
}
public function testGetAddressIsNull()
{
$user = new User();
$this->assertNull($user->getAddress());
}
}
Con esto tenemos testada la mayor pare de la funcionalidad. Ahora solo nos queda testear getAddress()
y asegurar que devuelve un objeto address, pero ¿como lo hacemos? La variable $address
es protected
por lo que no es fácil manejarla. Podríamos modificarla y hacerla pública. Entonces, después de instanciar un usuario, se puede configurar la variable $address
y ver lo que devuelve getAddress()
.
Pero ¿qué pasa si no podemos hacer esto? ¿qué pasa si no podemos modificar nuestra interfaz?, además en cierto modo es un poco «sucio» cambiar la visibilidad de una variable solo por hacer un test.
Bien, podemos utilizar la reflexión para cambiar la accesibilidad a las variables protected
de la siguiente manera.
public function testGetAddressIsAlreadySet()
{
$user = new User();
$user->setAddressId(123);
$reflection = new ReflectionClass('User');
$property = $reflection->getProperty('address');
$property->setAccessible(true);
$property->setValue($user, new Address(123));
$this->assertInstanceOf('Address', $user->getAddress());
}
Si ahora pasamos la cobertura de código, vemos como está mucho mejor, ya testeamos el método getAddress()
y además devuelve un objeto tipo address. El problema aquí es que no estamos testeando correctamente el método getAddress()
ya que realmente no se está estableciendo la propiedad address
y encima tenemos una linea «sucia».
Address es una dependencia dura y tenemos que cambiarla. Desde que instanciamos el objeto a traces del método no hay ninguna manera de inyectar un una dependencia para poder utilizar un mock, así que no podemos testar sin miedo a cargarla. En este ejemplo de juguete los efectos son inofensivos, tal vez solo sea una consulta SELECT con la que recuperar datos, pero eso tan simple ya transforma nuestro test y lo convierte en un test de integración quizás yo sea un paranoico, pero no quiero un test de integración, solo quiero probar mi código con un pequeño test unitario.
Una primera aproximación podría ser admitir el defecto y utilizar las anotaciones @codeCoverageIgnoreStart
e @codeCoverageIgnoreEnd
para no falsear la cobertura de código. Así quedaría el ejemplo cuando intentamos testar esta parte sin falsear la cobertura de código
// @codeCoverageIgnoreStart
if ($this->address === null) {
$this->address = new Address($this->getAddressId());
}
// @codeCoverageIgnoreEnd
Investigando acerca de como solventar esto ví que otra opción podría ser crear una clase UserTesting extend User
en la que poder rescribir los métodos para hacer más sencillo el testing. Pero al final, creo que estamos trabajando demasiado para mockear una simple clase.
Por último encontré el post“Mocking Hard Dependencies que es el que estoy adaptando en este post. En dicho post se utiliza Mockery. Mockery proporciona la capacidad de inyectar una clase en el ámbito del test cuando se invoca la clase con indicada o cuando se utiliza un método estático.
Esto que parece magi, funciona mejor cuando utilizamos autoload en nuestros tests. Lo que hace Mockery es que si se encuentra alguna llamada a un método de una clase que no ha sido cargada se utiliza el autoload para cargarlo. Con esto Mockery permite sobrecargar el comportamiento mediante la inyección de una clase con el mismo nombre, de modo que el autoload en vez de cargar la clase real carga nuestra clase Mock. Parece complicado, pero si estamos utilizando Composer todo será coser y cantar.
Quizás al intentarlo las primeras veces tengamos errores como este o peores:
Mockery\Exception\RuntimeException: Could not load mock Address, class already exists
No hay que desesperarse, este problema sucede debido a que la clase ya esta cargada en un test anterior, por ello Mockery no puede inyectar la clase con el mismo nombre.
Por suerte, gracias a PHPUnit tenemos una funcionalidad que permite que los test se ejecuten por separado evitando el problema anterior. Para ello solo tenemos que añadir las anotaciones @runInSeparateProcess
y @preserveGlobalState disable
, aquí más info.
Ahora nuestro test será algo como esto:
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testGetAddressHardDependency()
{
$addressMock = Mockery::mock('overload:Address');
$user = new User();
$user->setAddressId(123);
$this->assertInstanceOf('Address', $user->getAddress());
}
Utilizando ‘Overload’ en el mock Mockery crea una nueva clase llamada ‘Address’ y la inyecta en el ámbito del test. Entonces, cuando nosotros en el test llamamos a $user->getAddress()
e invocamos a new Address()
el objecto _Address creado utiliza Mockery para «hacerse pasar» por la clase Address, con ello no utilizamos la clase real. ¿Hemos mejorado? Ahora no tenemos que ‘ensuciar’ nuestro código, de la misma manera nuestra cobertura vuelve a ser correcta y encima todos los test están verdes 🙂
Si bien es cierto que este método es una manera efectiva de poder testar dependencias difíciles o como dice Google Translate <>, esto puede hacer que nuestros tests sean más frágiles. El mejor enfoque siempre es utilizar inyección de dependencias. Cuando trabajamos con legacy code esto a veces incluye un montón de refactorizaciones. Este método solo es aplicable si no podemos invertir tiempo en refactorizar o no es aplicable el coste de refactorizar (punto muy discutible) entonces este método podría ser viable.
¿Que haces cuando te enfrentas a cosas como esta? ¿No testas?, ¿haces test de integración para curarte en salud?