Escribiendo pruebas

Escribir pruebas para Nette Tester es único porque cada prueba es un script PHP que se puede ejecutar de forma independiente. Esto esconde un gran potencial. Ya cuando escribes la prueba, puedes ejecutarla fácilmente y averiguar si funciona correctamente. Si no, puedes depurarla fácilmente en el IDE y buscar el error.

Incluso puedes abrir la prueba en el navegador. Pero sobre todo, al ejecutarla, realizas la prueba. Inmediatamente descubres si pasó o falló.

En el capítulo introductorio mostramos una prueba realmente trivial del trabajo con arrays. Ahora crearemos nuestra propia clase que probaremos, aunque también será simple.

Comenzaremos con una estructura de directorios típica para una librería o proyecto. Es importante separar las pruebas del resto del código, por ejemplo, debido al deployment, porque no queremos subir las pruebas al servidor de producción. La estructura puede ser, por ejemplo, así:

├── src/           # código que probaremos
│   ├── Rectangle.php
│   └── ...
├── tests/         # pruebas
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

Y ahora crearemos los archivos individuales. Comenzaremos con la clase probada, que colocaremos 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 crearemos una prueba para ella. El nombre del archivo con la prueba debe coincidir con la máscara *Test.php o *.phpt, elegiremos, por ejemplo, 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 puedes ver, los llamados métodos de aserción como Assert::same() se utilizan para confirmar que el valor real corresponde al valor esperado.

Queda el último paso y es el archivo bootstrap.php. Este contiene código común para todas las pruebas, por ejemplo, autoloading de clases, configuración del entorno, creación de un directorio temporal, funciones auxiliares y similares. Todas las pruebas cargan el bootstrap y luego se dedican solo a las pruebas. El bootstrap puede verse de la siguiente manera:

<?php
require __DIR__ . '/vendor/autoload.php';   # carga el autoloader de Composer

Tester\Environment::setup();                # inicialización de Nette Tester

// y otras configuraciones (es solo un ejemplo, en nuestro caso no son necesarias)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

El bootstrap indicado presupone que el autoloader de Composer será capaz de cargar también la clase Rectangle.php. Esto se puede lograr, 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 nos revelará posibles errores de sintaxis y si no hay ningún error tipográfico, se imprimirá:

$ php RectangleTest.php

OK

Si cambiáramos la afirmación en la prueba a una falsa Assert::same(123, $rect->getArea()); sucedería esto:

$ php RectangleTest.php

Failed: 200.0 should be 123

in RectangleTest.php(5) Assert::same(123, $rect->getArea());

FAILURE

Al escribir pruebas, es bueno cubrir todas las situaciones límite. Por ejemplo, cuando la entrada sea cero, un número negativo, en otros casos, por ejemplo, una cadena vacía, null, etc. En realidad, te obliga a reflexionar y decidir cómo debe comportarse el código en tales situaciones. Las pruebas luego fijan el comportamiento.

En nuestro caso, un valor negativo debe lanzar una excepción, lo cual verificamos mediante Assert::exception():

// el ancho no debe ser negativo
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'The dimension must not be negative.',
);

Y agregamos una prueba similar para la altura. Finalmente, probamos que isSquare() devuelva true si ambas dimensiones son iguales. Intenta escribir tales pruebas como ejercicio.

Pruebas más claras

El tamaño del archivo con la prueba puede crecer y volverse rápidamente confuso. Por eso es práctico agrupar las áreas probadas individuales en funciones separadas.

Primero mostraremos una variante más simple, pero elegante, y es mediante la función global test(). Tester no la crea automáticamente para evitar colisiones si tuvieras una función con el mismo nombre en el código. La crea el método setupFunctions(), que debes llamar en el archivo bootstrap.php:

Tester\Environment::setup();
Tester\Environment::setupFunctions();

Mediante esta función podemos dividir el archivo de prueba de forma ordenada en unidades con nombre. Al ejecutarse, se imprimirán secuencialmente las descripciones.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('rectángulo general', function () {
	$rect = new Rectangle(10, 20);
	Assert::same(200.0, $rect->getArea());
	Assert::false($rect->isSquare());
});

test('cuadrado general', function () {
	$rect = new Rectangle(5, 5);
	Assert::same(25.0, $rect->getArea());
	Assert::true($rect->isSquare());
});

test('las dimensiones no deben ser negativas', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

	Assert::exception(
		fn() => new Rectangle(10, -1),
        InvalidArgumentException::class,
	);
});

Si necesitas ejecutar código antes o después de cada prueba, pásalo 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 el llamado TestCase, que es una clase donde las unidades individuales representan métodos cuyos nombres comienzan con 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);
	}
}

// Ejecución de los métodos de prueba
(new RectangleTest)->run();

Para probar las excepciones, esta vez usamos la anotación @throw. Aprenderás más en el capítulo TestCase.

Funciones auxiliares

Nette Tester contiene varias clases y funciones que pueden facilitarte, por ejemplo, la prueba del contenido de un documento HTML, la prueba de funciones que trabajan con archivos, etc.

Su descripción la encontrarás en la página Clases auxiliares.

Anotaciones y omisión de pruebas

La ejecución de las pruebas puede verse influenciada por anotaciones en forma de comentario phpDoc al principio del archivo. Puede verse, por ejemplo, así:

/**
 * @phpExtension pdo, pdo_pgsql
 * @phpVersion >= 7.2
 */

Las anotaciones indicadas dicen que la prueba debe ejecutarse solo con la versión de PHP 7.2 o superior y si están presentes las extensiones PHP pdo y pdo_pgsql. Estas anotaciones son seguidas por el ejecutor de pruebas desde la línea de comandos, que en caso de que las condiciones no se cumplan, omite la prueba y la marca en la salida con la letra s – skipped. Sin embargo, al ejecutar manualmente la prueba no tienen ningún efecto.

La descripción de las anotaciones la encontrarás en la página Anotaciones de pruebas.

La prueba también se puede omitir en función del cumplimiento de una condición propia mediante Environment::skip(). Por ejemplo, esta omitirá las pruebas en Windows:

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
	Tester\Environment::skip('Requires UNIX.');
}

Estructura de directorios

Recomendamos que en librerías o proyectos un poco más grandes se divida el directorio con las 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
	└── ...

Así podrás ejecutar las pruebas de un solo espacio de nombres o subdirectorio:

tester tests/NamespaceOne

Situaciones especiales

Una prueba que no llama a ningún método de aserción es sospechosa y se evaluará como errónea:

Error: This test forgets to execute an assertion.

Si realmente se considera válida una prueba sin llamadas a aserciones, llama por ejemplo a Assert::true(true).

También puede ser engañoso usar exit() y die() para terminar la prueba con un mensaje de error. Por ejemplo, exit('Error in connection') termina la prueba con el código de retorno 0, lo que indica éxito. Usa Assert::fail('Error in connection').