Nette Documentation Preview

syntax
Escribir pruebas
****************

.[perex]
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 |guide#¿Qué hace único a Tester?] 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 .{file: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 .{file:tests/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 |assertions] 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 .{file:tests/bootstrap.php}
<?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');
```

.[note]
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 |best-practices:composer#Autoloading] 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á:

/--pre .[terminal]
$ php RectangleTest.php

<span style="color:#FFF; background-color:#090">OK</span>
\--

Si cambiamos la aserción de la prueba por algo incorrecto, como `Assert::same(123, $rect->getArea());`, ocurre esto:

/--pre .[terminal]
$ php RectangleTest.php

<span style="color: #FFF">Failed: </span><span style="color: #FF0">200.0</span><span style="color: #FFF"> should be </span><span style="color: #FF0">123</span>

<span style="color: #CCC">in </span><span style="color: #FFF">RectangleTest.php(5)</span><span style="color: #808080"> Assert::same(123, $rect->getArea());</span>

<span style="color: #FFF; background-color: #900">FAILURE</span>
\--


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() |Assertions#Assert::exception()]:

```php .{file:tests/RectangleTest.php}
// 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`:

```php .{file:tests/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 .{file:tests/RectangleTest.php}
<?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:

```php
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`.

```php .{file:tests/RectangleTest.php}
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 |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 |helpers].


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:

```php .{file:tests/RectangleTest.php}
/**
 * @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 |running-tests], 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 |test-annotations].

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:

```php
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:

/--pre .[terminal]
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:

/--pre .[terminal]
<span style="color: #FFF; background-color: #900">Error: This test forgets to execute an assertion.</span>
\--

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')`.

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').