Utiliser le coeur de Composer pour les tests unitaires (suite)
Lors du dernier blogue, j'ai continué à présenter le fichier composer.json en introduisant les tests pouvant être exécutés dans le cadre de Composer. Le cadriciel Pest a été présenté. Ce cadriciel est en fait une couche par-dessus PHPUnit permettant de simplifier l'écriture de test et de rapprocher la syntaxe du language naturel.
Je vais maintenant aller plus en détails sur ce qu'il est possible de faire avec Pest avec quelques exemples concrets.
Pest pour chasser les vilains bogues
Comme j'ai mentionné dans le blogue précéden, un test de base s'écrit comme ceci :
test('perform sum', function () {
$result = sum(1, 2);
expect($result)->toBe(3);
});
Déjà il est possible de devenir plus efficient en remplacant le nom de la fonction test par it. C'est plus court et permet de faire que lors de l'affichage des résultats ça ressemble à une phrase.
it performs sums
Tests:
1 passed (1 assertions)
Duration: 0.05s
L'exemple précédent utilise l'Expectation API. Si vous préférez une approche plus traditionnelle, il est aussi possible d'utiliser l'Assertion API permettant de vérifier les conditions de façon plus traditionnelle et comme avec PHPUnit.
Voici un exemple où le résultat est le même mais Assert est utilisé:
test('perform sum', function () {
$result = sum(1, 2);
$this->assertEquals(3, $result);
});
Afin de prioriser une approche plus orientée vers l'humain, les prochains exemples se baseront sur l'utilisation de l'Expectation API.
Attentes
Comme vous avez pu voir dans le premier exemple, utiliser les attentes (expectation) implique de passer la valeur dans la fonction expect()
. Expect est une fonction globale qui retourne une instance de la classe Expectation. On utilise ensute une des méthodes de cette classe. Vous aurez l'embarras du choix ; il y en a 57 permettant de vérifier les comparaisons, la vérification de types, les opérations sur les chaînes de caractères et les tableaux, les vérifications booléennes et de valeurs nulles.
Il est même possible d'en chaîner plusieurs. Par exemple, on peut utiliser not-> avant toBe() afin d'inverser la condition. toBe()
est l'équivalent de ===
en php car il vérifie à la fois le type et la valeur.
test('hamlet', function () {
$valeur = "la question";
expect($valeur)
->toBe("la question")
->or(
expect($valeur)->not->toBe("la question")
);
});
Voici un exemple avec chaînage multiple :
test('chaînage des assertions', function () {
expect('Bonjour le monde')
->toBeString()
->toContain('Bonjour')
->toContain('monde')
->not->toBeEmpty();
});
Fonctions et modificateurs utiles
Voici la liste des fonctions accessibles et la page contenant les détails techniques sur chacune d’entre elles :
- toBe()
- toBeArray()
- toBeEmpty()
- toBeBetween()
- toBeTrue()
- toBeTruthy()
- toBeFalse()
- toBeFalsy()
- toBeGreaterThan()
- toBeGreaterThanOrEqual()
- toBeLessThan()
- toBeLessThanOrEqual()
- toContain()
- toContainEqual()
- toContainOnlyInstancesOf()
- toHaveCount()
- toHaveProperty()
- toHaveProperties()
- toMatchArray()
- toMatchObject()
- toEqual()
- toEqualCanonicalizing()
- toEqualWithDelta()
- toBeIn()
- toBeInfinite()
- toBeInstanceOf()
- toBeBool()
- toBeCallable()
- toBeFile()
- toBeFloat()
- toBeInt()
- toBeIterable()
- toBeNumeric()
- toBeDigits()
- toBeObject()
- toBeResource()
- toBeScalar()
- toBeString()
- toBeJson()
- toBeNan()
- toBeNull()
- toHaveKey()
- toHaveKeys()
- toHaveLength()
- toBeReadableDirectory()
- toBeReadableFile()
- toBeWritableDirectory()
- toBeWritableFile()
- toStartWith()
- toThrow()
- toEndWith()
- toMatch()
- toMatchConstraint()
- toBeUppercase()
- toBeLowercase()
- toBeAlpha()
- toBeAlphaNumeric()
- toBeSnakeCase()
- toBeKebabCase()
- toBeCamelCase()
- toBeStudlyCase()
- toHaveSnakeCaseKeys()
- toHaveKebabCaseKeys()
- toHaveCamelCaseKeys()
- toHaveStudlyCaseKeys()
- toHaveSameSize()
- toBeUrl()
- toBeUuid()
Leur nom indique assez clairement ce qu'elles font.
Sinon, il y a aussi des modificateurs (modifier en anglais) permettant de faire varier le comportement des fonctions précédentes :
- and()
- Permet de chaîner les vérifications et d'inclure une nouvelle valeur qui est vérifiée.
expect($id)->toBe(14) ->and($name)->toBe('Nuno');
- dd()
- Pour die and dump. Il s'agit d'une fonction utile pour débogguer car elle affiche à la console le contenu d'une variable et met fin aux tests.
- ddWhen() et ddUnless()
- Même chose que dd mais on ajoute une condition nécessaire pour faire dd.
- each()
- Fonction intéressante quand on veut tester plusieurs valeurs dans une tableau d'items.
expect([1, 2, 3])->each->toBeInt(); expect([1, 2, 3])->each->not->toBeString(); expect([1, 2, 3])->each(fn ($number) => $number->toBeLessThan(4));
- json()
- Fonction qui convertit une entité JSON en tableau php.
- match()
- not
- Comme mentionné plus haut, cela inverse la valeur booléene du résultat de la prochaine vérification.
- sequence()
- C'est pour faire plusieurs vérifications pour chaque valeur dans une liste. Donc, c'est comme each mais plusieurs vérifications seront appliquées à chaque éléments d'un tableau.
expect([1, 2, 3])->sequence( fn ($number) => $number->toBe(1), fn ($number) => $number->toBe(2), fn ($number) => $number->toBe(3), );
- C'est pour faire plusieurs vérifications pour chaque valeur dans une liste. Donc, c'est comme each mais plusieurs vérifications seront appliquées à chaque éléments d'un tableau.
- when() ou unless()
- Ils permettent de faire une vérification dans un test si une condition est vraie.
expect($user) ->when($user->is_verified === true, fn ($user) => $user->daily_limit->toBeGreaterThan(10)) ->email->not->toBeEmpty();
En lien avec une situation où vous développez des tests et vous ne voulez cibler qu'un test, la fonction only() permet de ne cibler qu'un seul test.
test('sum', function () {
$result = sum(1, 2);
expect($result)->toBe(3);
})->only();
Hooks ou événement qui déclenche une fonction
Voici https://pestphp.com/docs/hooks
-
beforeEach()
- Pour assigner des valeurs ou exécuter du code avant chacun des tests dans le fichier actuel où se trouve ce crochet (hook).
-
afterEach()
- Même chose mais c'est exécuté après chaque test.
-
beforeAll()
- C'est exécuté une fois avant tous les tests.
-
afterAll()
- C'est exécuté une fois après tous les tests.
Sauter des tests
On peut temporairement désactiver des tests spécifiques en rajoutant skip à la fin du test comme ceci :
it('has home', function () {
//
})->skip();
Pest va rajouter un message d'avertissement dans la console. On peut aussi rajouter un message en paramètres dans la fonction. Ce message sera affiché dans la console. On peut même rajouter une condition pour que le test soit sauté. En l'occurence, le premier paramètre devra contenir la condition. Vous pouvez aussi ajouter un message dans le second paramètres.
Ne pas tester dans certains environnements Si vous ne souhaitez pas lancer des tests sur un système d'exploitation spécifique, alors plusieurs fonctions sont présentes :
- skipOnWindows()
- skipOnMac()
- skipOnLinux()
Dans la logique inverse, vous pouvez lancer certains tests uniquement lorsque le système d'exploitation détecté est un sysème d'exploitation :
- onlyOnWindows()
- onlyOnMac()
- onlyOnLinux()
Vous pouvez aussi sauter certains tests en fonction de la version de PHP :
it('has home', function () {
//
})->skipOnPhp('>=8.0.0');
Tests qui doivent échouer
La fonction throws() s'attend à ce que le test échoue. On passe en paramètre l'exception qu'on s'attend de déclencher. Le test échoue s'il n'échoue pas.
it('throws exception', function () {
throw new Exception('Something happened.');
})->throws(Exception::class);
On peut aussi ajouter un message en deuxième paramètres.
Vous pouvez aussi utiliser throwsIf()
qui prend en premier paramètre une condition à vérifier. Alors il faut à la fois que le test lance une exception spécifique et répond à une condition pour qu'il soit réussi.
it('throws exception', function () {
})->throwsIf(fn() => DB::getDriverName() === 'mysql', Exception::class, 'MySQL is not supported.');
Et throwsUnless()
fait la logique inverse où le test passe s'il ne rempli pas une condition (la valeur est fausse).
Si on s'attend juste en général que le test échoue sans vouloir spécifier l'erreur, utilisez plutôt la méthode fails()
.
Sa variante fail()
permet de faire automatiquement échouer le test manuellement. Ça pourrait être utile quand on ne trouve pas d'autres moyens de faire échouer avec les fonctions disponibles pour vérifier une condition.
Utiliser un ensemble de données
Enfin, utilisez with()
pour passer un tableau de données classique ou un dictionnaire. Cela permet de faire comme each ou sequence mais les données sont passées différemment.
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with(['enunomaduro@gmail.com', 'other@example.com']);
it('has emails', function (string $name, string $email) {
expect($email)->not->toBeEmpty();
})->with([
['Nuno', 'enunomaduro@gmail.com'],
['Other', 'other@example.com']
]);
Enfin, Pest est un cadriciel de test permettant de rendre les tests plus accessibles et intéressants à réaliser qu'avec PHPUnit, à tout le moins pour les débutants ou les gens désirant réduire la complexité. Il semble permettre de faire vérifier une panoplie de conditions et offre même la possibilité d'utiliser PHPUnit au cas où il ne serait pas suffisant.
On peut spécifier des conditions quand on veut vérifier une condition particulière ou même quand lancer un test ou non. Il permet aussi de communiquer explicitement dans la console en utilisant tout simplement un paramètre de la fonction appelée.
Cependant, vous ne pourrez échapper PHPUnit quand vous devrez vérifier des conditions plus complexes et spécifiques. De plus, ce cadriciel est moins supporté que d'autres et pourrait vous limiter dans certaines conditions.
Néanmoins, vous avez toujours PHPUnit comme plan b et vous pouvez même utiliser PHPUnit en parallèle à l'intérieur de Pest. Cela offre donc une certaine assurance qu'utiliser Pest ne limite pas l'extensibilité de votre suite de tests.
Merci d'avoir pris le temps de me lire cette session et de m'avoir incité à en apprendre plus sur Composer ! Je devais justement l'apprendre et ça a créé une belle opportunité.
Références
- PestPHP, Writing Tests, https://pestphp.com/docs/writing-tests (Page consultée le 21 mars 2025).
- PestPHP, Expectations, https://pestphp.com/docs/expectations/ (Page consultée le 21 mars 2025).
- PHPUnit, Assertions, https://docs.phpunit.de/en/11.4/assertions.html (Page consultée le 21 mars 2025).
- PestPHP, Expectations (reprise), https://pestphp.com/docs/expectations/ (Page consultée le 21 mars 2025).
- PestPHP, Filtering Tests, https://pestphp.com/docs/filtering-tests#content-only (Page consultée le 21 mars 2025).
- PestPHP, Hooks - beforeEach, https://pestphp.com/docs/hooks#content-beforeeach (Page consultée le 21 mars 2025).
- PestPHP, Hooks - afterEach, https://pestphp.com/docs/hooks#content-aftereach (Page consultée le 21 mars 2025).
- PestPHP, Hooks - beforeAll, https://pestphp.com/docs/hooks#content-beforeall (Page consultée le 21 mars 2025).
- PestPHP, Hooks - afterAll, https://pestphp.com/docs/hooks#content-afterall (Page consultée le 21 mars 2025).
- PestPHP, Skipping Tests, https://pestphp.com/docs/skipping-tests (Page consultée le 21 mars 2025).
- PestPHP, Exceptions, https://pestphp.com/docs/exceptions (Page consultée le 21 mars 2025).
- PestPHP, Datasets, https://pestphp.com/docs/datasets (Page consultée le 21 mars 2025).
Commentaires