Ya hemos hablado bastantes veces sobre TDD, sobre como instalar PHPUnit, cómo utilizar PHPUnit y Silex para poder hacer Test Driven development, en definitiva ya hemos dado los primeros pasos. Ahora vamos a dar otro pequeño paso, aprenderemos algunos patrones para escribir mejores tests.
Hablaremos sobre buenas maneras de escribir assert
, formas distintas de estructurar y construir fixtures y de algunos trucos a la hora de escribir pruebas para legacy code (código legado).
Vamos a aprender estos patrones mediante pequeños ejemplos utilizando PHPUnit, pero podría ser aplicable a cualquier otro lenguaje, ya que todo esto solo es un conjunto de buenas prácticas para hacer tests unitarios.
Patrones para realizar assert con PHPUnit
Los assert
son la esencia de los tests, si no comprobamos mediante un assert o similar.
Vamos a ir viendo poco a poco distintos patrones de los más simples a algunos más complejos.
Un estado resultante(Resulting State Assertion)
La idea detrás de un assert
es crear un objeto, ejecutar algunas funciones y después comprobar su estado interno. Lo mejor es utilizar un ejemplo para ilustrarlo:
public function testSizeOfListItemsAddedToIt()
{
$list = array();
array_push($list,'something');
$this->assertEquals(1, sizeof($list));
}
En este test podemos ver como comprobamos que cuando creamos un array y le añadimos un elemento su tamaño después de añadir el elemento es 1. Si fuésemos algo paranoicos, o tuviésemos dudas con el constructor podríamos añadir un assert más justo después de crear para comprobar que el array se crea vacío y que al llamar a la función array_push añadimos un elemento.
Validando hipótesis (guard assertion)
Otra opción a la hora de hacer testing es hacer explícitas las “hipótesis” antes de invocar al método que queramos probar.
public function testListIsNoLongerEmptyAfterAddingAnItemToIt()
{
$list = array();
$this->assertTrue(empty($list)); //guard assertion
array_push($list,'something');
$this->assertFalse(empty($list)); //state verification
}
Vemos como podemos verificar que la lista al ante de ejecutar el método array_push está vacía y después de añadirle un elemento ya no lo está.
Lo normal es combinar los 2 patrones anteriores para nuestros tests, normalmente primero se confirma el estado del objeto antes de ejecutar el método a probar y después se valida el resultado obtenido.
Triangulación (Delta assertion)
De vez en cuando ocurre que tenemos que trabajar con código del que no tenemos pruebas, podemos llamar a funciones que no hemos probado, eso no es un problema, pero al final estamos delegando la responsabilidad de nuestra prueba en otra función.
Una solución común puede ser triangular, es decir probar la diferencia que hay entre el antes y después de probar.
public function testSizeOfListReflectsItemsAddedToIt()
{
$list = array();
$sizeBefore = count($list);
array_push($list,'something');
$this->assertEquals($sizeBefore + 1, count($list));
}
Este ejemplo puede parecer algo ridículo pero resume muy bien los fundamentos del patrón de triangulación para pruebas unitarias. Es cierto, que estas aserciones pueden llegar a ser complejas de escribir, pero se centran en la esencia de lo que se está poniendo a prueba.
Custom assertion
A veces necesitamos mucho código para poder invocar a la funciones que queremos probar (Mocks, Stubs,…). Cuando esto sucede es una buena idea extraer toda la asercion a un método con el fin de encapsular toda lógica compleja. Así ganaremos algo de legibilidad en el código e incluso podremos utilizar nuestra aserción en otros test si fuese necesario
public function testTimeslotsAreOnWeekdays()
{
$calendar = new MeetingCalendar();
//omitido: añadir citas al calendario hasta
//final de las horas de oficina el próximo viernes
$time = $calendar->nextAvailableStartingTime();
$this->assertIsDuringOfficeHoursOnWeekday($time);
}
protected function assertIsDuringOfficeHoursOnWeekday(DateTime $time)
{
// Aserción: Omitida por brevedad
}
Una razón muy común para utilizar este tipo de custom assertion es la capacidad de realizar diferentes tipos de fuzzy matching. Por ejemplo, si necesitamos comparar 2 objetos pero solo por un subconjunto de propiedades. Además creando custom assertion podemos hacer uqe se lancen diferentes mensajes en caso de que el test falle y así sabremos en que punto esta fallando nuestro test.
Interacción (Interaction Assertion)
El último patrón para las aserciones es Interaction Assertion. Esta es la aserción más divertida. Con este patrón no comprobamos los resultados del código, sino que verificamos que nuestro código interactua con el resto de objetos como esperamos que lo haga. Seguro que con un ejemplo lo vemos más fácil.
public function testPaperBoyShouldDeliverPapers()
{
$david = new MockCustomer();
$ana = new MockCustomer();
$paperboy = new PayperBoy();
$paperboy->addToRoute($david);
$paperboy->addToRoute($ana);
$this->assertTrue(in_array($david, $paperboy->deliever)); $this->assertTrue(in_array($ana, $paperboy->deliever));
}
}
Este test es solo un ejemplo y no debemos tenerlo demasiado en consideración, ya que solo nos sirve para ilustrar el patrón interacción. Como vemos estamos creando 2 clientes Mock que interactuan con paperboy.
En este ejemplo, vemos como interactúan entre sí clases Mock con clases que queremos crear. Más adelante veremos como utilizar los Mocks proporcionados por PHPUnit.
En la práctica podemos combinar todos estos patrones assert para poder testear SUT (código bajo test). De momento son suficientes patrones de aserciones, ahora analizaremos algunos patrones de setUp y tearDown, a la vez veremos como utilizar fixtures (accesorios)
Patrones Accesorios (fixtures patterns)
Los accesorios o fixtures son una parte importante de los tests. No siempre es trivial construir un objeto, sobre todo debido a sus dependencias con otras clases, por ello a veces tenemos muchos problemas para testear algunos métodos que interactúan con muchos objetos. Por suerte podemos encontrar un buen numero de patrones que nos ayudarán al la hora de poder chequear nuestro código.
Método de creación parametrizada (Parameterized Creation Method)
Muchos de los objetos “fixture” son los llamados Entidades (entity objects), debido a que representan algo que existe en business domain. Generalmente estos objetos tienen una gran cantidad de atributos y no todos ellos son necesarios para el test.
A continuación vemos un ejemplo de fixture bastante común.
public function setUp()
{
$alice = new Person();
$alice->setId(1);
$alice->setDni(111111);
$alice->setFirstName("Alice");
$alice->setLastName("Perez");
$pedro = new Person();
$pedro->setId(2);
$pedro->setDni(222222);
$pedro->setFirstName("Pedro");
$pedro->setLastName("Garcia");
$juan = new Person();
$juan->setId(3);
$juan->setDni(333333);
$juan->setFirstName("Juan");
$juan->setLastName("Lopez");
$alice->isInLoveWith($pedro);
}
El patrón “Método de creación parametrizada” ataca esta situación ocultando los atributos que no son importantes extrayendo a un método de creación separado y estableciendo valores ramdom / dirty. De esta manera utilizando el patrón de creación parametrizada se hace cargo de los atributos que no son esenciales.
Aquí vemos un ejemplo de como utilizar el patrón
public function setUp()
{
$alice = $this->createPerson("Alice", "Perez");
$pedro = $this->createPerson("Pedro", "Garcia");
$juan = $this->createPerson("Juan", "Lopez");
$alice->isInLoveWith($pedro);
}
public function createPerson($firsName, $lastName)
{
$person = new Person();
$person->setFirstName($firsName);
$person->setLastName($lastName);
$person->setId(uniqid());
$person->setDni(uniqid());
return $person;
}
Vemos como se ha simplificado la construcción de objetos utilizando el método createPerson, además se ha dado cierta legibilidad al código, ya que podemos ver de un simple vistazo lo que está ocurriendo en el método setup.
De esta manera solo tenemos que preocuparnos de los atributos que nos interesan a la hora de hacer un test, dejando como random/dirty el resto.
Objeto Madre (Object Mother)
Cuando llevamos a cabo una refactorización, una buena práctica utilizar el patrón “método de creación parametrizada” para evitar el código duplicado. Pero puede darse el caso que entre distintos test haya código duplicado de creación de objetos, asserts,… por ello el siguiente paso natural es mover estos métodos a una helper. El patrón Object Mother describe un agregado de la creación de tales métodos.
El objeto madre es un objeto o conjunto de objetos con las siguientes caráteristicas:
- Proporciona un business object totalmente formado, con todos los atributos.
- Devuelve el objeto solicitado en cualquier momento del ciclo de vida.
- Es facil personalizar el objeto entregado
- Permite que se actualice el objeto durante la prueba.
En resumen, el Object Mother es una especie de factoría que proporciona objetos con unas ciertas caracteristicas.
Con esto reducimos la duplicación de código en nuestros tests y hacemos todo el conjunto de test mucho más mantenible.
Debemos tener en cuenta que Object Mother es un patrón que dan muchas ganas de utilizarlo, ya que hay veces que nos encontramos mucho código repetido, pero hay que ir con cautela e ir haciendo refactorizaciones en pequeños pasos.
Automated TearDown
PHPUnit provee de un método teardown capaz de realizar una limpieza después de ejecutar un cada test. Ejemplos de cuando es necesaria una limpieza hay muchos eliminar objetos de la base de datos, restaurar archivos,…
Cuando el “desmontaje” de la lógica es complejo, o es necesario deshacer muchas cosas despues de un test, es hora de utilizar el patrón teardown, sobre todo para evitar errores en el futuro. Errores que son muy dificiles de rastrear y depurar.
La idea básica de este patrón no es limpiar en sí, sino crear un objeto “registro” (un simple array de referencias) donde ir apuntando los cambios que se van haciendo para poder deshacerlos más facilmente.
Normalmente este tipo de patrón es muy usado en test de integración.
Conclusiones
Hemos visto una serie de patrones que nos ayudarán a testar código, a tener un código más mantenible, evitar el código duplicado,… la mayor parte de este post está sacado de libro “Test Driven: Practical TDD and Acceptance TDD for Java Developers”, me he tomado la libertad de modificar los ejemplos para adaptarlos a PHP y PHPUnit.
Que pasa en el método de creación parametrizada, cuando las entidades no tienen el metodo setId como generalmente ocurre?
Me gustaMe gusta
Se puede crear el método, o crear una nueva clase que herede de la que queremos probar y en dicha clase crear el método. O hacer los assert sin tener en cuenta el id, solo con nombre.
Me gustaMe gusta
Un poco antiguo el comentario para responder pero es interesante el tema. generalmente… La gente cuando le asignan un ID fiscal no se lo auto-asigna porque no es su reponsabilidad, ya que ese ID es relativo a algo superior… ya sabemos… Reponsabilidad inversa etc. et.c SOLID. Suele haber un actor que te lo asigna Un Servicio común. son cosas que ocurren cuando se empiezan a complicar los servicios con millones de personas al final tienes que no permitir que ellos se gestionen su ID y mucho menos cuando es entre países AKA «macro»servicios
Me gustaMe gusta
Es un tema interesante tu lo has dicho, gracias por aportar 🙂
Me gustaMe gusta