Escribir pruebas
Escribir pruebas para Nette Tester es único porque cada prueba es un script PHP que se puede ejecutar por sí solo. Eso encierra un gran potencial. Mientras escribe una prueba, puede simplemente ejecutarla para comprobar si funciona correctamente. Si no funciona, puede recorrerla con facilidad en su IDE para encontrar el fallo.
Incluso puede abrir la prueba en un navegador. Pero lo más importante: al ejecutarla, ejecuta la prueba. Averigua de inmediato si pasó o falló.
En el capítulo introductorio mostramos una prueba muy trivial de manipulación de un array. Ahora crearemos nuestra propia clase para probarla, aunque también será sencilla.
Empecemos por una estructura de directorios típica de una biblioteca o un proyecto. Es importante separar las pruebas del resto del código, por ejemplo por motivos de despliegue, porque no queremos subir las pruebas al servidor de producción. La estructura puede tener este aspecto:
├── src/ # código que vamos a probar
│ ├── Rectangle.php
│ └── ...
├── tests/ # pruebas
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
Ahora creemos los distintos archivos. Empezaremos por la clase que vamos a probar, colocándola en el archivo
src/Rectangle.php:
<?php
class Rectangle
{
private float $width;
private float $height;
public function __construct(float $width, float $height)
{
if ($width < 0 || $height < 0) {
throw new InvalidArgumentException('The dimension must not be negative.');
}
$this->width = $width;
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
public function isSquare(): bool
{
return $this->width === $this->height;
}
}
Y le crearemos una prueba. El nombre del archivo de prueba debe encajar con el patrón *Test.php o
*.phpt; elegiremos la variante RectangleTest.php:
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
// rectángulo general
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # verificamos los resultados esperados
Assert::false($rect->isSquare());
Como ve, los métodos de aserción como Assert::same() sirven para afirmar que un
valor real coincide con el esperado.
El último paso es el archivo bootstrap.php. Contiene el código común a todas las pruebas, como la carga
automática de las clases, la configuración del entorno, la creación de un directorio temporal, funciones auxiliares y cosas por
el estilo. Todas las pruebas cargan el bootstrap y después se centran únicamente en probar. El bootstrap puede tener este
aspecto:
<?php
require __DIR__ . '/vendor/autoload.php'; # carga el autoloader de Composer
Tester\Environment::setup(); # inicialización de Nette Tester
// y otras configuraciones (solo un ejemplo, en nuestro caso no hace falta)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
Este bootstrap da por hecho que el autoloader de Composer sabrá cargar también la clase
Rectangle.php. Eso se consigue, por ejemplo, configurando la
sección autoload en composer.json, etc.
Ahora podemos ejecutar la prueba desde la línea de comandos como cualquier otro script PHP independiente. La primera ejecución revelará los posibles errores de sintaxis y, si no hay erratas, imprimirá:
$ php RectangleTest.php
OK
Si cambiamos la aserción de la prueba por algo incorrecto, como Assert::same(123, $rect->getArea());,
ocurre esto:
$ php RectangleTest.php Failed: 200.0 should be 123 in RectangleTest.php(5) Assert::same(123, $rect->getArea()); FAILURE
Al escribir pruebas es buena práctica cubrir todos los casos límite. Por ejemplo, entradas como el cero, los números negativos o, en otros escenarios, cadenas vacías, null, etc. Eso le obliga en realidad a pensar y a decidir cómo debe comportarse el código en esas situaciones. Las pruebas fijan después ese comportamiento.
En nuestro caso, un valor negativo debería lanzar una excepción, lo que verificamos con Assert::exception():
// la anchura no debe ser negativa
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
'The dimension must not be negative.',
);
Y añadimos una prueba parecida para la altura. Por último probamos que isSquare() devuelve true si
ambas dimensiones son iguales. Pruebe a escribir esas pruebas como ejercicio.
Pruebas bien organizadas
El tamaño del archivo de prueba puede crecer y volverse confuso rápidamente. Por eso resulta práctico agrupar las distintas áreas probadas en funciones separadas.
Veamos primero una opción más sencilla pero elegante que usa la función global test(). Tester no crea esta
función automáticamente para evitar colisiones si tiene en su código una función con el mismo nombre. La crea el método
setupFunctions(), al que debería llamar en su archivo bootstrap.php:
Tester\Environment::setup();
Tester\Environment::setupFunctions();
Con esta función podemos estructurar bien el archivo de prueba en unidades con nombre. Al ejecutarla, las etiquetas se imprimen una tras otra.
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
test('general rectangle', function () {
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
});
test('general square', function () {
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
});
test('dimensions must not be negative', function () {
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
);
Assert::exception(
fn() => new Rectangle(10, -1),
InvalidArgumentException::class,
);
});
Si necesita ejecutar código antes o después de cada test(), páseselo a la función setUp() o
tearDown(), respectivamente:
setUp(function () {
// código de inicialización que se ejecuta antes de cada test()
});
La segunda variante es orientada a objetos. Creamos un TestCase, es decir, una clase en la que las distintas unidades están
representadas por métodos cuyos nombres empiezan por test.
class RectangleTest extends Tester\TestCase
{
public function testGeneralOblong()
{
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
}
public function testGeneralSquare()
{
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
}
/** @throws InvalidArgumentException */
public function testWidthMustNotBeNegative()
{
$rect = new Rectangle(-1, 20);
}
/** @throws InvalidArgumentException */
public function testHeightMustNotBeNegative()
{
$rect = new Rectangle(10, -1);
}
}
// Ejecuta los métodos de prueba
(new RectangleTest)->run();
Esta vez hemos usado la anotación @throws para probar las excepciones. Puede saber más en el capítulo TestCase.
Funciones auxiliares
Nette Tester incluye varias clases y funciones que pueden facilitarle las pruebas, por ejemplo probar el contenido de un documento HTML, probar funciones que trabajan con archivos, etc.
Encontrará su descripción en la página Clases auxiliares.
Anotaciones y omisión de pruebas
La ejecución de las pruebas puede verse afectada por las anotaciones del comentario phpDoc que hay al principio del archivo. Por ejemplo, puede tener este aspecto:
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
Las anotaciones mostradas indican que la prueba solo debe ejecutarse con la versión 7.2 de PHP o superior y solo si están
presentes las extensiones pdo y pdo_pgsql. Estas anotaciones las interpreta el ejecutor de pruebas de la línea de comandos, que omite la prueba si no se cumplen las condiciones y
la marca en la salida con la letra s (skipped). Estas anotaciones, sin embargo, no tienen efecto cuando la prueba se
ejecuta manualmente.
Encontrará la descripción de las anotaciones en la página Anotaciones de pruebas.
Una prueba también se puede omitir según una condición propia con Environment::skip(). Por ejemplo, así se
omite la prueba en Windows:
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Requires UNIX.');
}
Estructura de directorios
Para bibliotecas o proyectos aunque sea un poco más grandes, recomendamos dividir el directorio de pruebas en subdirectorios según el espacio de nombres de la clase probada:
└── tests/
├── NamespaceOne/
│ ├── MyClass.getUsers.phpt
│ ├── MyClass.setUsers.phpt
│ └── ...
│
├── NamespaceTwo/
│ ├── MyClass.creating.phpt
│ ├── MyClass.dropping.phpt
│ └── ...
│
├── bootstrap.php
└── ...
Eso le permite ejecutar las pruebas de un único espacio de nombres, es decir, de un subdirectorio:
tester tests/NamespaceOne
Situaciones especiales
Una prueba que no llama a ningún método de aserción se considera sospechosa y se evalúa como error:
Error: This test forgets to execute an assertion.
Si una prueba sin aserciones es intencionadamente válida, llame a Assert::true(true) para marcarla como tal.
Usar exit() o die() para terminar una prueba con un mensaje de error puede ser engañoso. Por
ejemplo, exit('Error in connection') termina la prueba con el código de salida 0, que señala éxito. Use en su
lugar Assert::fail('Error in connection').