Écrire des tests
Écrire des tests pour Nette Tester a ceci d'unique que chaque test est un script PHP qui peut être exécuté seul. Cela recèle un grand potentiel. Pendant que vous écrivez un test, vous pouvez simplement le lancer pour vérifier qu'il fonctionne correctement. Si ce n'est pas le cas, vous pouvez facilement l'exécuter pas à pas dans votre IDE pour trouver le bug.
Vous pouvez même ouvrir le test dans un navigateur. Mais surtout : en le lançant, vous exécutez le test. Vous apprenez immédiatement s'il passe ou échoue.
Dans le chapitre d'introduction, nous avons montré un test très trivial portant sur la manipulation d'un tableau. Nous allons maintenant créer notre propre classe à tester, même si elle restera simple elle aussi.
Partons d'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 en vue du déploiement, car nous ne voulons pas envoyer les tests sur le serveur de production. La structure peut ressembler à ceci :
├── src/ # le code que nous allons tester
│ ├── Rectangle.php
│ └── ...
├── tests/ # les tests
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
Créons maintenant les différents fichiers. Nous commencerons 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 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';
// rectangle quelconque
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # vérifie les résultats attendus
Assert::false($rect->isSquare());
Comme vous le voyez, ce sont des méthodes d'assertion comme
Assert::same() qui servent à affirmer qu'une valeur réelle correspond à une valeur attendue.
La dernière étape est le fichier bootstrap.php. Il contient le code commun à tous les tests, comme
l'autoloading des classes, la configuration de l'environnement, la création d'un répertoire temporaire, des fonctions
auxiliaires et ainsi de suite. Tous les tests chargent le bootstrap et se concentrent ensuite uniquement sur le test. Le bootstrap
peut ressembler à ceci :
<?php
require __DIR__ . '/vendor/autoload.php'; # charge l'autoloader de Composer
Tester\Environment::setup(); # initialisation de Nette Tester
// et d'autres configurations (c'est juste un exemple, inutile dans notre cas)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
Ce bootstrap suppose que l'autoloader de Composer saura charger aussi la classe Rectangle.php. On
peut y parvenir, par exemple, en configurant la section
autoload dans composer.json, etc.
Nous pouvons désormais lancer le test depuis la ligne de commande comme n'importe quel autre script PHP autonome. Le premier lancement révélera d'éventuelles erreurs de syntaxe et, s'il n'y a aucune faute de frappe, il affichera :
$ php RectangleTest.php
OK
Si nous remplaçons l'assertion du test par une assertion incorrecte, comme
Assert::same(123, $rect->getArea());, voici ce qui se passe :
$ php RectangleTest.php Failed: 200.0 should be 123 in RectangleTest.php(5) Assert::same(123, $rect->getArea()); FAILURE
Quand on écrit des tests, il est bon de couvrir tous les cas limites. Par exemple des entrées comme zéro, des nombres négatifs ou, dans d'autres situations, des chaînes vides, null, etc. Cela vous oblige en fait à réfléchir et à décider comment le code doit se comporter dans ces situations. Les tests figent ensuite ce comportement.
Dans notre cas, une valeur négative doit lever une exception, ce que nous vérifions avec 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 semblable pour la hauteur. Enfin, nous testons que isSquare() renvoie true
si les deux dimensions sont identiques. Essayez d'écrire ces tests à titre d'exercice.
Des tests bien organisés
La taille du fichier de test peut augmenter et devenir vite confuse. Il est donc pratique de regrouper les différents domaines testés dans des fonctions distinctes.
Regardons d'abord une option plus simple, mais élégante, à l'aide de la fonction globale test(). Tester ne
crée pas cette fonction automatiquement, afin d'éviter les collisions si vous avez dans votre code une fonction du même nom.
Elle est créée par la méthode setupFunctions(), que vous devriez appeler dans votre fichier
bootstrap.php :
Tester\Environment::setup();
Tester\Environment::setupFunctions();
Grâce à cette fonction, nous pouvons structurer joliment le fichier de test en unités nommées. À l'exécution, les libellés seront affichés les uns après les autres.
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
test('rectangle quelconque', function () {
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
});
test('carré quelconque', 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 respectivement aux fonctions
setUp() ou tearDown() :
setUp(function () {
// code d'initialisation exécuté avant chaque test()
});
La seconde variante est orientée objet. Nous créons ce qu'on appelle un TestCase, c'est-à-dire une classe où les
différentes unités 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é l'annotation @throws pour tester les exceptions. Vous en apprendrez davantage dans
le chapitre TestCase.
Fonctions utilitaires
Nette Tester contient plusieurs classes et fonctions qui peuvent faciliter les tests, par exemple tester le contenu d'un document HTML, tester des fonctions qui travaillent avec des fichiers, et ainsi de suite.
Vous trouverez leur description sur la page Classes utilitaires.
Annotations et tests ignorés
L'exécution des tests peut être influencée par des annotations placées dans le commentaire phpDoc au début du fichier. Cela peut par exemple ressembler à ceci :
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
Les annotations présentées indiquent que le test ne doit être exécuté qu'avec PHP 7.2 ou une version plus récente et
uniquement si les extensions PHP pdo et pdo_pgsql sont présentes. Ces annotations sont interprétées
par le lanceur de tests en ligne de commande, qui ignore le test si les
conditions ne sont pas remplies et le marque de la lettre s (skipped) dans la sortie. Ces annotations n'ont cependant
aucun effet lorsque le test est lancé manuellement.
Vous trouverez la description des annotations sur la page Annotations de test.
Un test peut aussi être ignoré selon une condition personnalisée, à l'aide d'Environment::skip(). Ceci, par
exemple, ignore le test sous Windows :
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Nécessite UNIX.');
}
Structure des répertoires
Pour les bibliothèques ou les projets un tant soit peu importants, nous recommandons de diviser le répertoire des 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 pouvez ainsi exécuter les tests d'un seul espace de noms, c'est-à-dire d'un sous-répertoire :
tester tests/NamespaceOne
Situations particulières
Un test qui n'appelle aucune méthode d'assertion est considéré comme suspect et sera évalué comme une erreur :
Error: This test forgets to execute an assertion.
Si un test sans assertions est intentionnellement valide, appelez Assert::true(true) pour le signaler.
Utiliser exit() ou die() pour terminer un test avec un message d'erreur peut induire en erreur. Par
exemple, exit('Erreur de connexion') termine le test avec un code de sortie 0, ce qui signale un succès. Utilisez
plutôt Assert::fail('Erreur de connexion').