Tests d'écriture

L'écriture de tests pour Nette Tester est unique en ce sens que chaque test est un script PHP qui peut être exécuté de manière autonome. Cela présente un grand potentiel. Lorsque vous écrivez le test, vous pouvez simplement l'exécuter pour voir s'il fonctionne correctement. Si ce n'est pas le cas, vous pouvez facilement le parcourir dans l'IDE et rechercher un bug.

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

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

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

├── src/           # code that we will test
│   ├── Rectangle.php
│   └── ...
├── tests/         # tests
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

Et maintenant, nous allons créer des fichiers individuels. Nous allons commencer 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('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;
	}
}

Et nous allons créer un test pour elle. Le nom du fichier de test doit correspondre au masque *Test.php ou *.phpt, nous choisirons la variante RectangleTest.php:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

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

Comme vous pouvez le constater, les méthodes d'assertion telles que Assert::same() sont utilisées pour affirmer qu'une valeur réelle correspond à une valeur attendue.

La dernière étape consiste à créer le fichier bootstrap.php. Il contient un code commun pour tous les tests. Par exemple, le chargement automatique des classes, la configuration de l'environnement, la création d'un répertoire temporaire, les aides et autres. Chaque test charge le bootstrap et ne s'occupe que du test. Le bootstrap peut ressembler à :

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

Tester\Environment::setup(); # initialisation du testeur Nette

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

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

Nous pouvons maintenant exécuter le test à partir de la ligne de commande comme tout autre script PHP autonome. La première exécution révélera toute erreur de syntaxe, et si vous n'avez pas fait de faute de frappe, vous verrez :

$ php RectangleTest.php

OK

Si nous changeons dans le test la déclaration en false Assert::same(123, $rect->getArea());, ceci se produira :

$ php RectangleTest.php

Failed: 200.0 should be 123

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

FAILURE

Lorsque l'on écrit des tests, il est bon d'attraper toutes les situations extrêmes. Par exemple, si l'entrée est zéro, un nombre négatif, dans d'autres cas 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 corrigent ensuite le comportement.

Dans notre cas, une valeur négative devrait lever une exception, que nous vérifions avec Assert::exception():

// la largeur ne doit pas être un nombre négatif
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 à titre d'exercice.

Tests bien agencés

La taille du fichier de test peut augmenter et devenir rapidement encombrante. Il est donc pratique de regrouper les différentes zones testées dans des fonctions distinctes.

Tout d'abord, nous allons montrer une variante plus simple mais élégante, en utilisant la fonction globale test(). Le testeur ne la crée pas automatiquement, pour éviter une collision si vous aviez une fonction avec le même nom dans votre code. Il est uniquement créé par la méthode setupFunctions(), que vous appelez dans le fichier bootstrap.php :

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

Grâce à cette fonction, nous pouvons diviser joliment le fichier de test en unités nommées. Lors de l'exécution, les étiquettes seront affichées l'une après l'autre.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('general oblong', 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 vous devez exécuter le code avant ou après chaque test, passez-le à setUp() ou tearDown() :

setUp(fonction () {
	// code d'initialisation à exécuter avant chaque test()
}) ;

La deuxième variante est l'objet. Nous allons créer ce qu'on appelle un TestCase, qui est une classe où les unités individuelles sont représentées par des méthodes dont le nom commence 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();

Cette fois, nous avons utilisé une annotation @throw pour tester les exceptions. Consultez le chapitre TestCase pour plus d'informations.

Fonctions d'aide

Nette Tester comprend plusieurs classes et fonctions qui peuvent vous faciliter les tests, par exemple, des aides pour tester le contenu d'un document HTML, pour tester les fonctions de travail avec les fichiers, etc.

Vous pouvez trouver une description de ces fonctions sur la page Helpers.

Annotation et saut de tests

L'exécution des tests peut être affectée par des annotations dans le commentaire phpDoc au début du fichier. Par exemple, cela pourrait ressembler à ceci :

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

Les annotations indiquent que le test ne doit être exécuté qu'avec la version 7.2 ou supérieure de PHP et si les extensions PHP pdo et pdo_pgsql sont présentes. Ces annotations sont contrôlées par l'exécuteur de test en ligne de commande, qui, si les conditions ne sont pas remplies, saute le test et le marque avec la lettre s – sauté. Cependant, elles n'ont aucun effet lorsque le test est exécuté manuellement.

Pour une description des annotations, voir Test Annotations.

Le test peut également être ignoré en fonction de sa propre condition avec Environment::skip(). Par exemple, nous allons ignorer ce test sous Windows :

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

Structure des répertoires

Pour les bibliothèques ou les projets un peu plus importants, nous recommandons de diviser le répertoire de test en sous-répertoires en fonction de 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 des tests à partir d'un seul sous-répertoire de l'espace de noms :

tester tests/NamespaceOne

Cas limites

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 le test sans appel aux assertions doit vraiment être considéré comme valide, appelez par exemple Assert::true(true).

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