Explicamos qué son los Data Providers de PHPUnit y por qué son tan importantes para mejorar el diseño y mantenimiento de las pruebas unitarias. Ejemplos de uso de data providers.
En este artículo vamos a abordar una utilidad muy recurrida en el desarrollo de pruebas, que incluye PHPUnit, así como otros frameworks de test, los proveedores de datos.
Si no tienes idea de este framework de pruebas para PHP te recomendamos acceder a la home de contenidos sobre PHPUnit en DesarrolloWeb.com.
Por qué necesitamos los Data Providers
¿Alguna vez te has encontrado escribiendo pruebas en PHPUnit y repitiendo el mismo código con ligeras variaciones? No es algo muy raro y casi todos hemos pasado por ahí, copiando y pegando tests como si no hubiera un mañana.
Si no te viene a la cabeza una situación donde puedas haberte visto en esa necesidad de copiar y pegar un método de test con distintas variaciones, imagínate que tienes que probar una función que convierte temperaturas de Celsius a Fahrenheit. Claro, podrías escribir una prueba para 0°C, otra para 100°C, otra para -40°C... y terminar con una clase de test gigante lleno de código duplicado.
Obviamente copiar y pegar un mismo método cambiando solamente los datos es algo que debes evitar, ya que te llevará a un mal diseño de tus clases de test. Afortunadamente PHPUnit ofrece la funcionalidad de los data providers, mediante la cual puedes meter todos esos casos en una misma prueba, sin hacer malabares con foreach o estructuras de datos creadas al vuelo.
Qué son los data providers
Los Data Providers no son más que declaraciones de juegos de datos que podemos usar en métodos de test, para alimentar un mismo método con distintos conjuntos de datos de entrada. Estos conjuntos de datos se definen en una función que es la denominada "data provider" y luego existirán métodos que los usen para su ejecución.
Son muy útiles cuando queremos hacer varias pruebas y lo único que cambian son los datos con los que se tienen que probar las clases (el subject under test). En estos casos no necesitamos duplicar un método "n" veces para cada conjunto de datos, sino crear un data provider y usarlo en un método de test.
Cómo crear un data provider
Un juego de datos se declara como un método estático que devuelve un array. Es tan sencillo como el código que podemos ver a continuación, aunque luego también plantearemos algunas mejoras.
public static function additionProvider(): array {
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 3],
];
}
Cómo usar un data provider
Una vez que hemos creado el proveedor de datos podemos definir un método que lo use. Ese método se invocará automáticamente una vez por cada juego de datos que haya en el data provider.
Podemos declarar el uso de los data providers de diversos modos, pero vamos a explicar el más moderno, que tienes disponible en las últimas versiones de PHPUnit, que está basado en los atributos de PHP.
Los atributos de PHP son lo que en otros lenguajes llamamos decoradores o anotaciones. En PHP los han llamado "Atributos" que es un nombre un poco confuso porque nos hace pensar en los atributos de los objetos. Este mecanismo fue introducido en PHP 8.0 y se usa en PHPUnit desde la versión 10.
Lo primero que necesitaremos hacer es importar el atributo que sirve para decorar los métodos que usan los data providers.
use PHPUnit\Framework\Attributes\DataProvider;
Luego, para definir que un método usará el data provider tenemos que decorarlo usando ese atributo DataProvider
, de este modo:
#[DataProvider('additionProvider')]
public function testAdd(int $a, int $b, int $expected): void {
$this->assertSame($expected, $a + $b);
}
Cada uno de los ítem del conjunto de datos se irán asignando como parámetros en el método que usa el data provider, en el orden en el que están declarados.
Cómo hacer data providers más expresivos
Como hemos dicho, el framwework PHPUnit se encargará de ejecutar el método de test repetidas veces. Pero entonces, si uno de los juegos de datos da un fallo en el test ¿Cómo nos informará de qué item del array es el que ha dado el fallo?
La respuesta es bien simple, por medio del índice. Esto puede ser suficiente en muchas ocasiones, pero a veces resulta más conveniente aportar un nombre suficientemente expresive a cada juego de test, lo que nos permitirá identificarlo mejor.
Para indicar en el data provider el nombre expresivo que identifique a cada juego de datos usamos los índices del array del data provider.
public static function additionProvider(): array {
return [
'adding zeros' => [0, 0, 0],
'zero plus one' => [0, 1, 1],
'one plus zero' => [1, 0, 1],
'one plus one' => [1, 1, 3],
];
}
Como puedes ver, las llaves (claves del array asociativo devuelto por el data provider) en los arrays del data providers nos permiten expresar de manera clara qué se está testeando en cada caso. De este modo, cuando un caso de test falle, el propio framework será capaz de decirte qué estabas intentando probar.
Otro ejemplo de Data Provider de PHPUnit
Los data providers pueden ser tan complejos como sea necesario. Vamos a ver un segundo ejemplo para poder practicar un poco más con ellos.
public static function provideTextAndExpectedHours(): array {
return [
'220 words, default speed' => [str_repeat("word ", 220), 200, '0:02'],
'600 words, default speed' => [str_repeat("word ", 600), 200, '0:03'],
'50000 words, default speed' => [str_repeat("word ", 50000), 200, '4:10'],
'600 words, custom speed' => [str_repeat("word ", 600), 300, '0:02'],
];
}
#[DataProvider('provideTextAndExpectedHours')]
public function testReadTimeInHours(string $text, int $wordsPerMinute, string $expectedHours) {
$calculator = new ReadTimeCalculator($text, $wordsPerMinute);
$this->assertEquals($expectedHours, $calculator->getReadTimeInHours());
}
En el data provider anterior se crea un texto para calcular el tiempo de lectura. Luego se define las palabras por minuto que se consigue leer y por último el tiempo de lectura que nos debería devolver.
Por ejemplo, [str_repeat("word ", 600), 200, '0:03']
creará un texto de 600 palabras, define que se leen 200 palabras por minuto y nos debería devolver que necesita 3 minutos para completar la lectura.
El método recibe esos datos por orden. En la cabecera del método lo puedes comprobar los argumentos: (string $text, int $wordsPerMinute, string $expectedHours)
.
Conclusión
Los data providers de PHPUnit son una utilidad esencial para mejorar el código y el diseño de nuestras clases de test. Como has visto, te permiten definir múltiples conjuntos de datos sin ensuciar tu código y ejecutar un método para cada uno de los juegos de datos suministrados.
Esto lo hace el propio framework de manera automática, informando convenientemente qué ha fallado. Para ello puede usar las llaves del array o el índice del array donde está el juego de datos que ha hecho fallar la prueba.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...