Écriture des tests

L'écriture de tests pour Nette Tester est unique en ce que chaque test est un script PHP qui peut être exécuté indépendamment. Cela recèle un grand potentiel. Déjà lorsque vous écrivez le test, vous pouvez simplement l'exécuter et vérifier s'il fonctionne correctement. Sinon, il peut être facilement débogué pas à pas dans l'IDE et l'erreur recherchée.

Vous pouvez même ouvrir le test dans un navigateur. Mais surtout – en l'exécutant, vous effectuez le test. Vous savez immédiatement s'il a réussi ou échoué.

Dans le chapitre d'introduction, nous avons montré un test vraiment trivial de manipulation de tableau. Maintenant, nous allons créer notre propre classe que nous allons tester, même si elle sera également simple.

Commençons par une structure de répertoires typique pour une bibliothèque ou un projet. Il est important de séparer les tests du reste du code, par exemple pour le déploiement, car nous ne voulons pas télécharger les tests sur le serveur de production. La structure peut être par exemple comme ceci :

├── src/           # code que nous allons tester
│   ├── Rectangle.php
│   └── ...
├── tests/         # tests
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

Et maintenant, créons les fichiers individuels. Commençons par la classe testée, que nous placerons dans le fichier 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('La dimension ne doit pas être négative.');
		}
		$this->width = $width;
		$this->height = $height;
	}

	public function getArea(): float
	{
		return $this->width * $this->height;
	}

	public function isSquare(): bool
	{
		return $this->width === $this->height;
	}
}

Et créons un test pour elle. Le nom du fichier de test doit correspondre au masque *Test.php ou *.phpt, choisissons par exemple la variante RectangleTest.php :

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// rectangle général
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());  # nous vérifions les résultats attendus
Assert::false($rect->isSquare());

Comme vous pouvez le voir, les soi-disant méthodes d'assertion comme Assert::same() sont utilisées pour confirmer qu'une valeur réelle correspond à une valeur attendue.

Il reste une dernière étape, le fichier bootstrap.php. Il contient le code commun à tous les tests, par exemple l'autoloading des classes, la configuration de l'environnement, la création d'un répertoire temporaire, les fonctions d'aide et similaires. Tous les tests chargent le bootstrap et se consacrent ensuite uniquement aux tests. Le bootstrap peut ressembler à ceci :

<?php
require __DIR__ . '/vendor/autoload.php';   # charge l'autoloader Composer

Tester\Environment::setup();                # initialisation de Nette Tester

// et autres configurations (ce n'est qu'un exemple, dans notre cas, elles ne sont pas nécessaires)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

Le bootstrap indiqué suppose que l'autoloader Composer sera capable de charger également la classe Rectangle.php. Cela peut être réalisé par exemple en configurant la section autoload dans composer.json, etc.

Nous pouvons maintenant exécuter le test depuis la ligne de commande comme n'importe quel autre script PHP autonome. La première exécution nous révélera d'éventuelles erreurs de syntaxe et si aucune faute de frappe n'est présente, il s'affichera :

$ php RectangleTest.php

OK

Si nous changions l'assertion dans le test en une affirmation fausse Assert::same(123, $rect->getArea());, voici ce qui se passerait :

$ php RectangleTest.php

Failed: 200.0 should be 123

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

FAILURE

Lors de l'écriture des tests, il est bon de couvrir toutes les situations limites. Par exemple, lorsque l'entrée est zéro, un nombre négatif, dans d'autres cas peut-être une chaîne vide, null, etc. En fait, cela vous oblige à réfléchir et à décider comment le code doit se comporter dans de telles situations. Les tests fixent ensuite le comportement.

Dans notre cas, une valeur négative doit lever une exception, ce que nous vérifions à l'aide de Assert::exception() :

// la largeur ne doit pas être négative
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'La dimension ne doit pas être négative.',
);

Et nous ajoutons un test similaire pour la hauteur. Enfin, nous testons que isSquare() renvoie true si les deux dimensions sont identiques. Essayez d'écrire de tels tests comme exercice.

Tests plus clairs

La taille du fichier de test peut augmenter et devenir rapidement illisible. C'est pourquoi il est pratique de regrouper les différentes zones testées dans des fonctions distinctes.

Montrons d'abord une variante plus simple, mais élégante, à l'aide de la fonction globale test(). Tester ne la crée pas automatiquement, pour éviter les collisions si vous aviez une fonction du même nom dans votre code. Elle est créée par la méthode setupFunctions(), que vous appelez dans le fichier bootstrap.php :

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

À l'aide de cette fonction, nous pouvons bien structurer le fichier de test en ensembles nommés. Lors de l'exécution, les descriptions seront affichées progressivement.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('rectangle général', function () {
	$rect = new Rectangle(10, 20);
	Assert::same(200.0, $rect->getArea());
	Assert::false($rect->isSquare());
});

test('carré général', function () {
	$rect = new Rectangle(5, 5);
	Assert::same(25.0, $rect->getArea());
	Assert::true($rect->isSquare());
});

test('les dimensions ne doivent pas être négatives', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

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

Si vous avez besoin d'exécuter du code avant ou après chaque test, passez-le à la fonction setUp() resp. tearDown() :

setUp(function () {
	// code d'initialisation qui s'exécute avant chaque test()
});

La deuxième variante est orientée objet. Nous créons un soi-disant TestCase, qui est une classe où les ensembles individuels représentent des méthodes dont les noms commencent par 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);
	}
}

// Exécution des méthodes de test
(new RectangleTest)->run();

Pour tester les exceptions, nous avons cette fois utilisé l'annotation @throw. Vous en apprendrez plus dans le chapitre TestCase.

Fonctions d'aide

Nette Tester contient plusieurs classes et fonctions qui peuvent vous faciliter par exemple le test du contenu d'un document HTML, le test de fonctions travaillant avec des fichiers, etc.

Leur description se trouve sur la page Classes d'aide.

Annotations et saut de tests

L'exécution des tests peut être influencée par des annotations sous forme de commentaire phpDoc au début du fichier. Elle peut ressembler par exemple à ceci :

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

Les annotations indiquées disent que le test doit être exécuté uniquement avec la version PHP 7.2 ou supérieure et si les extensions PHP pdo et pdo_pgsql sont présentes. Ces annotations sont suivies par le lanceur de tests depuis la ligne de commande, qui, dans le cas où les conditions ne sont pas remplies, saute le test et le marque dans la sortie par la lettre s – skipped. Cependant, lors de l'exécution manuelle du test, elles n'ont aucune influence.

La description des annotations se trouve sur la page Annotations de test.

Un test peut également être sauté en fonction de la satisfaction d'une condition personnalisée à l'aide de Environment::skip(). Par exemple, celle-ci saute les tests sous Windows :

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

Structure des répertoires

Nous recommandons, pour les bibliothèques ou projets un peu plus grands, de diviser le répertoire de tests en sous-répertoires selon l'espace de noms de la classe testée :

└── tests/
	├── NamespaceOne/
	│   ├── MyClass.getUsers.phpt
	│   ├── MyClass.setUsers.phpt
	│   └── ...
	│
	├── NamespaceTwo/
	│   ├── MyClass.creating.phpt
	│   ├── MyClass.dropping.phpt
	│   └── ...
	│
	├── bootstrap.php
	└── ...

Vous pourrez ainsi exécuter les tests d'un seul espace de noms, c'est-à-dire d'un sous-répertoire :

tester tests/NamespaceOne

Situations spéciales

Un test qui n'appelle aucune méthode d'assertion est suspect et sera évalué comme erroné :

Error: This test forgets to execute an assertion.

Si un test sans appel d'assertions doit vraiment être considéré comme valide, appelez par exemple Assert::true(true).

Il peut également être délicat d'utiliser exit() et die() pour terminer un test avec un message d'erreur. Par exemple, exit('Error in connection') termine le test avec le code de retour 0, ce qui signale un succès. Utilisez Assert::fail('Error in connection').