Un code PHP sans tests en 2026, c'est un code qui vieillit mal. Voici la stack que je déploie sur tous mes projets, et les raisons derrière chaque choix.
La pyramide qui marche
- 70 % unit tests : rapides, isolés, font tourner la CI en 30 secondes.
- 25 % integration tests : vraie DB, vraies dépendances, plus lents mais indispensables.
- 5 % end-to-end : Playwright ou Panther, scénarios critiques uniquement.
Pest : l'expérience d'écriture
Pest a révolutionné mon quotidien de tests PHP. Syntaxe à la Jest, helpers intelligents, output lisible.
it('calculates total with VAT', function () {
$invoice = new Invoice([100, 50]);
expect($invoice->totalWithVat(20))->toBe(180.0);
});
it('rejects negative amounts', function () {
expect(fn() => new Invoice([-10]))
->toThrow(InvalidArgumentException::class);
});Dataset (fka data providers) est devenu mon outil de prédilection pour les tests paramétrés :
it('parses dates', function ($input, $expected) {
expect(parseDate($input))->toEqual($expected);
})->with([
['2026-01-15', '15 janvier 2026'],
['15/01/2026', '15 janvier 2026'],
['Jan 15, 2026', '15 janvier 2026'],
]);PHPUnit : l'intégration
Pour les tests qui touchent à la DB, au framework, à des services externes mockés, je reste sur PHPUnit classique. Pest peut faire les deux mais la structure de PHPUnit est plus familière à la majorité des devs sur ce périmètre.
Utilisez RefreshDatabase (Laravel) ou un dump SQL minimal que vous chargez avant chaque classe de test. Évitez la factory massive qui ralentit tout.
Infection : la qualité de vos tests
Un taux de couverture élevé ne dit rien. Infection mute votre code (change des opérateurs, inverse des conditions) et vérifie que vos tests cassent. Si aucun test ne casse sur une mutation, vous avez un bug non détecté.
# Exemple : remplacer >= par >
-if ($amount >= $threshold) {
+if ($amount > $threshold) {Si aucun test ne relève le changement, vous manquez un edge case. Infection vous le dit. C'est l'outil qui a le plus fait progresser la qualité de mes tests en 2 ans.
Ma config CI
# .github/workflows/ci.yml
- run: composer install --no-dev --optimize-autoloader
- run: vendor/bin/pest --parallel --coverage --min=80
- run: vendor/bin/phpstan analyse --level=9
- run: vendor/bin/infection --threads=4 --min-msi=70 --only-coveredLes anti-patterns à éviter
- Mocker tout, y compris la DB : vous testez votre mock, pas votre code.
- Écrire des tests après coup "pour la couverture" : inutile.
- Tester le framework : si Eloquent marche, pas besoin de tester que
save()sauve. - Tests lents qu'on exécute manuellement : sans CI rapide, personne ne les lance.