Découvrez de manière structurée l’utilisation avancée de Pest, devenu la valeur par défaut depuis Laravel 11 : Expectation API, datasets, fakes et Mockery.
Pest est un framework de test construit au-dessus de PHPUnit et adopté par défaut dans tout nouveau projet depuis Laravel 11. Il reprend l’ensemble des assertions de PHPUnit tout en offrant une syntaxe concise basée sur des closures.Principales différences avec PHPUnit :
Aspect
Pest
PHPUnit
Définition de test
closures test() / it()
méthodes dans une classe
Assertions
Expectation API (expect()->toBe())
$this->assert*()
Datasets
dataset() / with()
@dataProvider
Hooks
beforeEach() / afterEach()
setUp() / tearDown()
Imbrication
regroupement via describe()
séparation par classe
Les tests Pest étant exécutés par PHPUnit, ils cohabitent sans problème avec des tests PHPUnit existants. Lancez-les avec php artisan test ou vendor/bin/pest.
Permet une écriture proche du langage naturel, sous forme « it should… ».
it('redirige les utilisateurs non authentifiés vers la page de connexion', function () { $response = $this->get('/dashboard'); $response->assertRedirect('/login');});
Regroupe des tests apparentés. Utile pour partager un beforeEach() ou structurer logiquement un fichier.
describe('gestion des commandes', function () { beforeEach(function () { $this->user = User::factory()->create(); $this->actingAs($this->user); }); it('récupère la liste des commandes', function () { Order::factory(3)->for($this->user)->create(); $response = $this->get('/orders'); $response->assertOk()->assertJsonCount(3, 'data'); }); it("ne récupère pas les commandes des autres utilisateurs", function () { $other = User::factory()->create(); Order::factory()->for($other)->create(); $response = $this->get('/orders'); $response->assertOk()->assertJsonCount(0, 'data'); });});
describe() peut être imbriqué, mais une imbrication trop profonde nuit à la lisibilité. Deux niveaux au maximum sont un bon repère.
it("chaque plan utilisateur a sa propre limite", function (string $plan, int $limit) { $user = User::factory()->create(['plan' => $plan]); expect($user->requestLimit())->toBe($limit);})->with([ ['free', 100], ['pro', 1000], ['enterprise', 10000],]);
use App\Services\PaymentGateway;use Mockery\MockInterface;test('le service de paiement est invoqué', function () { $mock = $this->mock(PaymentGateway::class, function (MockInterface $mock) { $mock->expects('charge') ->with(1000, 'eur') ->andReturn(['status' => 'succeeded']); }); $result = app(PaymentGateway::class)->charge(1000, 'eur'); expect($result['status'])->toBe('succeeded');});
Contrairement à un mock, un spy exécute l’implémentation tout en enregistrant les appels.
use App\Services\NotificationService;test("vérifier que le service de notifications a été appelé", function () { $spy = $this->spy(NotificationService::class); $this->post('/orders', ['amount' => 1000]); $spy->shouldHaveReceived('send')->once()->with('order.created');});
Mocker certaines méthodes et laisser les autres utiliser l’implémentation réelle.
use App\Services\ReportService;use Mockery\MockInterface;test("l'écriture du fichier de rapport est passée en mock", function () { $mock = $this->partialMock(ReportService::class, function (MockInterface $mock) { $mock->expects('writeFile')->andReturnNull(); }); $result = $mock->generate(); expect($result)->not->toBeNull();});
use Illuminate\Http\Client\ConnectionException;test("gestion d'une erreur de connexion", function () { Http::fake(fn () => throw new ConnectionException('Connection refused')); $result = app(UserApiClient::class)->findWithFallback(1); expect($result)->toBeNull();});
use Illuminate\Support\Facades\Event;use App\Events\OrderCreated;test("un événement est dispatché à la création d'une commande", function () { Event::fake(); $this->post('/orders', ['product_id' => 1, 'amount' => 1000]); Event::assertDispatched(OrderCreated::class, function ($event) { return $event->order->amount === 1000; });});
Vous pouvez ne fake que certains événements et laisser les autres s’exécuter réellement.
use Illuminate\Support\Facades\Notification;use App\Notifications\OrderShipped;test("une notification est envoyée lors de l'expédition", function () { Notification::fake(); $user = User::factory()->create(); $order = Order::factory()->for($user)->create(); $this->post("/orders/{$order->id}/ship"); Notification::assertSentTo($user, OrderShipped::class, function ($notification) use ($order) { return $notification->order->id === $order->id; });});
use Illuminate\Foundation\Testing\RefreshDatabase;test("un utilisateur peut être créé", function () { $user = User::factory()->create(['name' => 'Laravel']); expect(User::count())->toBe(1); expect($user->name)->toBe('Laravel');});
Là où RefreshDatabase prépare les migrations pour tous les tests, LazilyRefreshDatabase diffère les migrations jusqu’à ce qu’un test modifie effectivement la base. Cela accélère les suites où de nombreux tests ne touchent pas à la base.
Pour la plupart des projets, RefreshDatabase est un choix sûr. Envisagez LazilyRefreshDatabase uniquement quand la suite est grande et comporte beaucoup de tests sans base de données.
# Afficher la couverture dans le terminalphp artisan test --coverage# Forcer un seuil minimum (échec CI en dessous)php artisan test --coverage --min=80# Générer un rapport HTMLvendor/bin/pest --coverage-html=coverage/# Identifier les tests lentsphp artisan test --profile
La mesure de couverture allonge significativement le temps d’exécution des tests. En CI, il est recommandé de la faire dans un job dédié ou uniquement lors des pull requests.