Haciendo testing unitario al 100%. Hace unos días estuvimos hablando de como refactorizar controladores para separar responsabilidades. Ahora llega la hora de dar un paso más y testear unitariamente las clases que creamos.
La idea detrás de este post es que modifiquemos una clase ya construida y funcional, llenarla de tests y refactorizarla para que los test solo testeemos una clase y no necesiten más que phpunit, ni base de datos ni más clases de las necesarias.
Para aprender a hacer este tipo de refactor y otros del estilo, lo mejor es buscar katas, existen muchas páginas donde encontrar katas, solveet por ejemplo, otra opcion es buscar en github «kata» y bucear entre los resultados.
Nos ayudaremos de PHPStorm para que nuestro proceso sea más sencillo. Además hemos creado un pequeño repositorio en github con el que hacer katas en PHP de manera sencilla, ya hay algunas resueltas y otras más por resolver: https://github.com/jeslopcru/php-coding-dojo
En este post hemos optado por la kata: TripServiceKata es una kata en la que aprenderemos como separar las responsabilidades de una clase de servicio para no utilizar base de datos ni más clases de las necesarias. En un primer momento tenemos una clase funcional, pero si la intentamos testear tendremos una serie de problemas comunes (la kata esta pensada para eso) que iremos resolviendo.
Recubriendo la clase para poder testearla
El primer inconveniente que tenemos al intentar hacer una instancia de la clase TripService es que necesitamos un «User», no es inconveniente, solo tenemos que crearlo y listo. Pero en el momento que intentamos <> con la clase para testearla nos empezamos a encontrar con los primeros inconvenientes
class TripServiceTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function it_does_something()
{
$user = new User('Jesus');
$tripService = new TripServiceKata($user);
$tripService->getTripsByUser($user);
}
}
Así que el primer paso será crear un clase recubrimiento que extienda a TripService para amoldar a nuestros tests.
No tenemos que adustarnos ante estos cambios, solo hemos creado una clase TripServiceKataCover que extiende a la clase que queremos testar, hemos creado 2 funciones protected (remarcamos protected porque con esa visibilidad podremos modificarla de manera sencilla en nuestros tests). Para crear las funciones getLoggedUser y getTripList nos hemos ayudado de las funciones de PHPStorm de refactor «extract to method». Así que en resumidas cuentas, no hemos tocado nada, solo hemos envuelto la clase para hacernos más cómodo el refactor.
El primer test
Aunque hemos estado jugando, o al menos intentando testar «a pelo» la clase TripService hasta darnos cuenta que necesitamos un recubrimiento, todavía no hemos testado nada. Así que vamos a por el primer test. Ya hemos visto el código, así que lo primero que haremos será no entrar en el bucle, sino ir a por la excepción. Esto nos ayudará a que vayamos calentando.
/** @test */
public function ifUserNotLoggedThrowException()
{
$this->setExpectedException('TripServiceKata\Exception\UserNotLoggedInException');
$tripService = new TripServiceKataCover(null);
$user = new User('Jesus');
$tripService->getTripsByUser($user);
}
Ya hemos conseguido nuestro primer verde, así que lo que haremos será comitear para seguir adelante. Lo mejor es ir haciendo pequeños commits, por si nos liamos y llegamos a callejones sin salida no tengamos que desperdiciar mucho trabajo.
Siguiendo con los tests
Analizando un poco el código, nos encontramos con que si el usuario no tiene amigos, devolvemos una lista vacía, es decir si al crear TripServiceKataCover en vez de null, le pasamos una instancia de usuario, tendremos devuelta una lista vacía. Así que vamos:
/** @test */
public function whenUsersHasNoFriendsReturnAnEmptyList()
{
$loggedUser = new User('Marta');
$tripService = new TripServiceKataCover($loggedUser);
$user = new User('Jesus');
$result = $tripService->getTripsByUser($user);
$this->assertEquals([], $result);
}
Ya podemos meternos con el test complicado, conocemos como funciona todo el algoritmo, así que podemos crear una serie de User, Trip y demás que nos ayude a testear el bucle. Una ayuda para saber si estamos testando todo, es utilizar «Code coverage» no como medida de sino para saber por que ramas del código NO estamos pasando con lo que esto nos ayudará a saber si nuestros test pasan por todas las bifurcaciones.
Así que ahora creamos todo lo necesario para el test:
/** @test */
public function whenUsersHasFriendsReturnTripListOfAFriend()
{
$loggedUser = new User('LoggedUserName');
$jesus = new User('Jesus');
$jesus->addFriend($loggedUser);
$trip = new Trip();
$jesus->addTrip($trip);
$tripService = new TripServiceKataCover($loggedUser);
$result = $tripService->getTripsByUser($jesus);
$this->assertEquals([$trip], $result);
}
Con esto ya tenemos nuestra clase testada, pero eso no quiere decir que hayamos terminado. Ahora lo que hacemos es comitear, ya que tenemos los tests en verde.
Ahora es necesario que hagamos una fase de refactoring sobre todo de los tests. Podemos ver mucho código repetido, muchas inicializaciones, vamos, ahora tenemos los tests un poco «guarros», es necesario que los tests queden sencillos. La idea a tener en mente cuando escribimos código y refactorizamos, es que estamos escribiendo código para que lo lean nuestros compañeros (o peor aun para que lo lea nuestro yo del futuro), no la máquina
Ahora podemos crear un método setUp para los test, crear atributos de la clase de test y dejar los test entendibles.
Listo, con esto ya hemos creado una serie de test sobre código legacy, ahora nos será más fácil añadir nueva funcionalidad o modificar la existente. ¿Como te enfrentas a tener que añadir funcionalidad a clases PHP legacy? ¿tus test unitarios son verdaderamente unitarios?
Un comentario en “Haciendo testing unitario al 100”