Top Code 2024, les challenges sont de nouveau disponibles dans les boards pour les participant(e)s => Boards

Tests unitaires en PHP : prendre en main Pest

Découvrir Pest PHP, l’alternative à PHPUnit, en écrivant une dizaine de tests unitaires.

→ Corrigé du Challenge : Collectionneur de figurines

Qu’est ce que Pest PHP ?

Pest est un framework de tests unitaires pour PHP qui propose une syntaxe différente de celle de PHPUnit. En s’inspirant notamment de ce qui existe côté javascript, avec Jest. Dans le moteur, il s’appuie sur PHPUnit, donc il peut être considéré comme une surcouche de PHPUnit. D’ailleurs, lancer la commande « .\vendor\bin\pest » permet de lancer à la fois des tests de Pest ET de PHPUnit (l’inverse n’est pas vrai).

Pest se présente comme un framework « élégant », la syntaxe est en effet plus directe et fonctionne avec l’écriture de fonctions et de closures plutôt qu’en créant des objets comme dans PHPUnit.

Partons à la découverte de ce framework de tests unitaires !

Au programme :

On crée une classe PHP

Pour présenter les premières fonctionnalités de Pest, on va s’appuyer sur le challenge débutant COLLECTION_1. Le code complet est disponible sur Github.

Ce challenge propose de déterminer le prix d’une collection de figurine. Selon le nombre d’exemplaires de la figurine, celle-ci vaut 15 ou 30€ à l’achat. A la revente, c’est son prix d’achat, multiplié par une côte.

Figurine.php

<?php
declare(strict_types=1);

namespace Challenges\COLLECTION_1;

final class Figurine
{
    private const LIMIT_RARE = 2000;
    private const PRICE_BUY_RARE = 30;
    private const PRICE_BUY_NOT_RARE = 15;

    private int $priceBuy;

    public function __construct(
        private int $exemplaires,
        private float $cote
    )
    {
        $this->calculPriceBuy();
    }

    private function calculPriceBuy(): void
    {
        if ($this->exemplaires < self::LIMIT_RARE) {
            $this->priceBuy = self::PRICE_BUY_RARE;
            return;
        }

        $this->priceBuy = self::PRICE_BUY_NOT_RARE;
    }

    public function getPriceBuy(): int
    {
        return $this->priceBuy;
    }

    public function getPriceSell(): float
    {
        return $this->priceBuy * $this->cote;
    }
}

Un peu d’explications :

  • Je crée des constantes de classe pour enregistrer les valeurs clés du challenge
  • J’utilise la promotion de propriétés dans le constructeur
  • J’ai une méthode privée pour calculer le prix d’achat
  • Et 2 getters pour pouvoir retourner le prix d’achat et le prix de vente

Premiers tests unitaires avec Pest PHP

Je vais travailler dans le fichier : ./pests/COLLECTION_1/FigurineTest.php

Les fichiers de tests de Pest doivent être suffixés par Test.php.

Premiers tests, j’instancie mon objet et je fais une assertion :

<?php
declare(strict_types=1);

use Challenges\COLLECTION_1\Figurine;

test('un prix d\'achat à 15€', function() {

    $figurine = new Figurine(exemplaires: 5000, cote: 1);

    $this->assertEquals(
        15,
        $figurine->getPriceBuy()
    );
});

test('un prix d\'achat à 30€', function() {

    $figurine = new Figurine(exemplaires: 500, cote: 1);

    $this->assertEquals(
        30,
        $figurine->getPriceBuy()
    );
});

Un peu d’explications :

  • Les tests commencent par la fonction test() ou it(). Je préfère test() car j’écris le descriptif de mes tests en français. Sinon, à l’affichage (cf. plus bas), cela donne « it bla bla bla… » ce qui ne veut pas dire grand chose.
  • Le premier paramètre est donc une chaine de caractères qui décrit le test.
  • Ensuite on passe une closure avec le contenu de notre test
  • La méthode assertEquals est la même que dans PHPUnit
  • PHP 8 : j’utilise les paramètres nommés à la construction de mes objets

Et voici la sortie dans la console à l’exécution des tests :

Les 2 premiers tests avec Pest PHP !

De la même façon, on peut tester le prix de vente :

test('un prix de vente à partir d\'une figurine non rare', function() {

    $figurine = new Figurine(exemplaires: 5000, cote: 2.5);

    $this->assertEquals(
        15 * 2.5,
        $figurine->getPriceSell()
    );
});

test('un prix de vente à partir d\'une figurine rare', function() {

    $figurine = new Figurine(exemplaires: 500, cote: 1.5);

    $this->assertEquals(
        30 * 1.5,
        $figurine->getPriceSell()
    );
});

Dataprovider avec Pest PHP

Là j’ai testé 1 seule information à chaque fois. On a la possibilité de passer un lot de données, qui vont valider le même test. Pour le prix d’achat, j’ai vérifié qu’une figurine produite à 5000 exemplaires valait bien 15€. Mais qu’en est il d’une figurine produite à 2000 ? 2001 ? 3000 ? 10000 ? 50000 ? Etc.

On peut écrire un dataprovider à l’aide de la méthode with() :

test('un prix d\'achat à 15€ pour plein de nombres d\'exemplaires', function($exemplaires) {

    $figurine = new Figurine(exemplaires: $exemplaires, cote: 1);

    $this->assertEquals(
        15,
        $figurine->getPriceBuy()
    );
})->with(
    [2000, 2001, 2500, 5000, 10000, 50000]
);

test('un prix d\'achat à 30€ pour plein de nombres d\'exemplaires', function($exemplaires) {

    $figurine = new Figurine(exemplaires: $exemplaires, cote: 1);

    $this->assertEquals(
        30,
        $figurine->getPriceBuy()
    );
})->with(
    [1, 20, 50, 500, 1999]
);

Un peu d’explications :

  • On a précisé un paramètre à notre closure, ici $exemplaires
  • On appelle donc ce paramètre dans la fonction
  • Puis on chaine la méthode with() qui prend en paramètre un tableau avec toutes les données à tester
  • On écrit ici le test pour les figurines non rares, puis pour les figurines rares. On a donc pu tester quelques cas limites (1999 et 2000).

Autre façon de passer des données à un test avec Pest PHP

Pour résoudre le challenge, je crée maintenant une classe « Collection » qui contiendra toutes les figurines.

<?php
declare(strict_types=1);

namespace Challenges\COLLECTION_1;

class Collection
{
    /**
     * @var array<int, Figurine> $figurines
     */
    private array $figurines = [];

    /**
     * @param array<int, int> $informationsExemplaires
     * @param array<int, float> $informationsCotes
     */
    public function __construct(array $informationsExemplaires, array $informationsCotes)
    {
        foreach ($informationsExemplaires as $key => $exemplaires) {
            $this->figurines[] = new Figurine(
                exemplaires: $exemplaires,
                cote: $informationsCotes[$key]
            );
        }
    }

    /**
     * @return array<int, Figurine>
     */
    public function getFigurines(): array
    {
        return $this->figurines;
    }

    public function getTotalPriceBuy(): int
    {
        $total = 0;

        foreach ($this->figurines as $figurine) {
            $total += $figurine->getPriceBuy();
        }

        return $total;
    }

    public function getTotalPriceSell(): float
    {
        $total = 0;

        foreach ($this->figurines as $figurine) {
            $total += $figurine->getPriceSell();
        }

        return $total;
    }

    public function getTotalDifference(): float
    {
        return $this->getTotalPriceSell() - $this->getTotalPriceBuy();
    }
}

Un peu d’explications :

  • Le constructeur prend comme informations les données du challenge
  • La PHPDoc est indiquée pour pouvoir faire fonctionner PHPStan correctement
  • J’ai plusieurs getters qui me retournent des éléments ou informations différents

Pour tester chacune de mes méthodes, et garder 1 seule assertion par test, je vrais créer des informations au début de mon fichier de test, que je vais passer à chacun de mes tests, à l’aide de « use », dans un fichier CollectionTest.php :

<?php
declare(strict_types=1);

use Challenges\COLLECTION_1\Collection;
use Challenges\COLLECTION_1\Figurine;

/**
 * Jeux de données
 */
$exemplaires = [50, 5000];
$cotes = [1.5, 2.5];
$collection = new Collection($exemplaires, $cotes);

test('on instancie bien des Figurines', function() use ($collection) {

    $this->assertInstanceOf(
        Figurine::class,
        $collection->getFigurines()[0]
    );
});

test('total d\'achat', function() use ($collection) {

    $this->assertEquals(
        30 + 15,
        $collection->getTotalPriceBuy()
    );
});

test('total du prix de vente',  function() use ($collection) {
    
    $this->assertEquals(
        30 * 1.5 + 15 * 2.5,
        $collection->getTotalPriceSell()
    );
});

Un peu d’explications :

  • Le premier test permet de vérifier que $this->figurines contient bien des objets Figurine.
  • Les 2 tests suivants vérifient que les totaux sont corrects

Tester le bon déroulé du challenge

Depuis la page d’un challenge, tu peux récupérer un jeu de données complet, avec la valeur attendue, idéal pour écrire un test !

/**
 * Jeux de données complexe issu de Tainix diretement
 */
test('jeu de données complet', function() {

    $exemplaires = [50, 50, 50000, 2000, 50000, 2000, 2000, 2000, 50000, 2000, 2000, 50000, 50000, 2000, 2000, 2000, 50000];
    $cotes = [2, 8, 1, 0.6, 1, 1.2, 1, 0.6, 0.6, 1, 1, 1, 0.8, 1.2, 1, 1, 0.6];

    $collection = new Collection($exemplaires, $cotes);

    $this->assertEquals(
        219, // Réponse fournie par Tainix
        $collection->getTotalDifference()
    );
});

Et voilà la sortie complète dans la console :

Tout ce vert !

Conclusion sur les premiers usages de Pest PHP

Voici pour un premier tour d’horizon de Pest PHP ! On a vu :

  • Comment créer un fichier de test (suffixé Test.php)
  • Ecrire un premier test « autonome »
  • Passer des données par un dataprovider
  • Passer des données par le use

Voici le lien vers la doc de Pest : https://pestphp.com/

Pour ceux et celles qui voudraient comparer avec PHPUnit, les tests ont aussi été écrits avec PHPUnit. Les fichiers sont disponibles sur Github. L’important n’est pas la librairie utilisée, l’important c’est d’être à l’aise et efficace avec les outils qu’on utilise. Et que son code soit le plus couvert possible par des tests.

Pour aller + loin, n’hésite pas à choisir un challenge de code sur lequel mettre tout ça en pratique ! Ces autres contenus mettent également Pest en avant :

PS : Si tu utilises la sandbox PHP, tu as tout de suite à ta disposition les 2 librairies de tests unitaires : PHPUnit et Pest.


Qui a codé ce superbe contenu ?

Keep learning

Autres contenus à découvrir


Ta newsletter chaque mois

Corrigés, challenges, actualités, veille technique... aucun spam.