Skip to Content
LaravelTesting

Testing — Pest PHP Laravel 12

Framework de testing moderne avec syntaxe expressive et élégante.

🚀 Installation

# Installer Pest composer require pestphp/pest --dev --with-all-dependencies composer require pestphp/pest-plugin-laravel --dev # Initialiser php artisan pest:install # Lancer les tests php artisan test ./vendor/bin/pest

Structure

tests/ ├── Feature/ # Tests fonctionnels (HTTP) ├── Unit/ # Tests unitaires (logique isolée) ├── Pest.php # Configuration globale └── TestCase.php # Classe de base

Configuration Pest.php

<?php // tests/Pest.php use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; // Test case pour Feature uses(TestCase::class, RefreshDatabase::class)->in('Feature'); // Test case pour Unit uses(TestCase::class)->in('Unit'); // Helpers globaux function actingAsUser($user = null) { return test()->actingAs($user ?? User::factory()->create()); } function createPost($attributes = []) { return Post::factory()->create($attributes); }

🎯 Tests basiques

Création

php artisan make:test UserTest --unit --pest php artisan make:test PostTest --pest

Syntaxe Pest vs PHPUnit

<?php // Pest (moderne) test('user has full name', function () { $user = new User(['first_name' => 'John', 'last_name' => 'Doe']); expect($user->full_name)->toBe('John Doe'); }); it('can check if user is admin', function () { $user = User::factory()->make(['role' => 'admin']); expect($user->isAdmin())->toBeTrue(); }); // PHPUnit (ancien) class UserTest extends TestCase { /** @test */ public function user_has_full_name() { $user = new User(['first_name' => 'John', 'last_name' => 'Doe']); $this->assertEquals('John Doe', $user->full_name); } }

🎨 Expectations

Base

// Égalité expect($value)->toBe(10); // === expect($value)->toEqual(10); // == expect($array)->toMatchArray(['key' => 'value']); // Booléens expect($active)->toBeTrue(); expect($deleted)->toBeFalse(); expect($value)->toBeTruthy(); expect($value)->toBeFalsy(); // Types expect($user)->toBeInstanceOf(User::class); expect($name)->toBeString(); expect($count)->toBeInt(); expect($items)->toBeArray(); // Null et vide expect($value)->toBeNull(); expect($value)->not->toBeNull(); expect($array)->toBeEmpty(); // Comparaisons expect($age)->toBeGreaterThan(18); expect($age)->toBeLessThan(100); expect($price)->toBeBetween(10, 100); // Strings expect($email)->toContain('@'); expect($url)->toStartWith('https://'); expect($string)->toMatch('/^[a-z]+$/i'); expect($text)->toHaveLength(10); // Arrays expect($items)->toHaveCount(5); expect($array)->toHaveKey('name'); expect($array)->toContain('value');

Séquences

expect(['a', 'b', 'c'])->sequence( fn ($item) => $item->toBe('a'), fn ($item) => $item->toBe('b'), fn ($item) => $item->toBe('c'), );

🌐 Tests Feature

Routes et controllers

<?php // tests/Feature/PostTest.php use App\Models\User; use App\Models\Post; it('can list posts', function () { Post::factory()->count(3)->create(); $this->get('/api/posts') ->assertOk() ->assertJsonCount(3); }); it('can create a post', function () { $user = User::factory()->create(); $this->actingAs($user) ->post('/api/posts', [ 'title' => 'Test Post', 'content' => 'Content here', ]) ->assertCreated() ->assertJson(['title' => 'Test Post']); expect(Post::count())->toBe(1); }); it('validates post creation', function () { $user = User::factory()->create(); $this->actingAs($user) ->post('/api/posts', []) ->assertUnprocessable() ->assertJsonValidationErrors(['title', 'content']); }); it('can update a post', function () { $user = User::factory()->create(); $post = Post::factory()->for($user)->create(); $this->actingAs($user) ->put("/api/posts/{$post->id}", ['title' => 'Updated Title']) ->assertOk(); expect($post->fresh()->title)->toBe('Updated Title'); }); it('cannot delete other users posts', function () { $user = User::factory()->create(); $otherUser = User::factory()->create(); $post = Post::factory()->for($otherUser)->create(); $this->actingAs($user) ->delete("/api/posts/{$post->id}") ->assertForbidden(); expect(Post::count())->toBe(1); });

Assertions HTTP

// Status ->assertOk() // 200 ->assertCreated() // 201 ->assertAccepted() // 202 ->assertNoContent() // 204 ->assertNotFound() // 404 ->assertForbidden() // 403 ->assertUnauthorized() // 401 ->assertUnprocessable() // 422 // JSON ->assertJson(['key' => 'value']) ->assertJsonPath('data.0.name', 'John') ->assertJsonCount(3, 'data') ->assertJsonStructure([ 'data' => ['*' => ['id', 'name']], ]) // Database expect(User::count())->toBe(1); $this->assertDatabaseHas('users', ['email' => 'john@example.com']); $this->assertDatabaseMissing('users', ['deleted' => true]);

🎭 Datasets

Dataset basique

it('validates emails', function (string $email, bool $valid) { expect(isValidEmail($email))->toBe($valid); })->with([ ['valid@example.com', true], ['invalid-email', false], ['test@test.co.uk', true], ['@invalid.com', false], ]); it('calculates discount correctly', function (int $price, int $discount, int $expected) { expect(calculateDiscount($price, $discount))->toBe($expected); })->with([ [100, 10, 90], [200, 20, 160], [50, 50, 25], ]);

Datasets nommés

dataset('emails', [ 'valid gmail' => 'test@gmail.com', 'valid corporate' => 'john@company.com', 'invalid format' => 'not-an-email', ]); dataset('roles', [ 'admin' => 'admin', 'editor' => 'editor', 'user' => 'user', ]); it('creates user with role', function (string $role) { $user = User::factory()->create(['role' => $role]); expect($user->role)->toBe($role); })->with('roles');

🎪 Hooks

BeforeEach / AfterEach

beforeEach(function () { $this->user = User::factory()->create(); $this->actingAs($this->user); }); afterEach(function () { // Cleanup }); it('can access user', function () { expect($this->user)->toBeInstanceOf(User::class); }); it('is authenticated', function () { $this->assertAuthenticated(); });

BeforeAll / AfterAll

beforeAll(function () { // Setup une fois pour tous }); afterAll(function () { // Cleanup une fois après tous });

🔥 Mocking et Faking

Facades

use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Event; use App\Mail\WelcomeEmail; it('sends welcome email', function () { Mail::fake(); $user = User::factory()->create(); $user->sendWelcomeEmail(); Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) { return $mail->hasTo($user->email); }); }); it('dispatches event', function () { Event::fake(); $user = User::factory()->create(); Event::assertDispatched(UserRegistered::class); }); it('queues job', function () { Queue::fake(); ProcessVideo::dispatch($video); Queue::assertPushed(ProcessVideo::class); });

Mockery

use App\Services\PaymentGateway; it('processes payment successfully', function () { $gateway = Mockery::mock(PaymentGateway::class); $gateway->shouldReceive('charge') ->once() ->with(100, 'token_123') ->andReturn(['success' => true]); $service = new PaymentService($gateway); $result = $service->processPayment(100, 'token_123'); expect($result['success'])->toBeTrue(); });

🎯 Higher Order Tests

it('has users') ->expect(User::count()) ->toBeGreaterThan(0); it('user has posts') ->expect(fn () => User::factory()->create()) ->posts ->toBeEmpty(); it('throws exception') ->expect(fn () => User::findOrFail(999)) ->throws(ModelNotFoundException::class);

🧩 Plugins

Faker

it('creates user with fake data', function () { $user = User::create([ 'name' => $this->faker->name(), 'email' => $this->faker->email(), ]); expect($user)->toBeInstanceOf(User::class); });

Laravel Expectations

it('has custom expectations', function () { $user = User::factory()->create(['role' => 'admin']); expect($user) ->toBeModel(User::class) ->role->toBe('admin'); });

🔧 Organisation

Groupes

it('is in admin group', function () { // Test admin logic })->group('admin', 'auth'); it('is slow test', function () { // Long running test })->group('slow');
pest --group=admin pest --exclude-group=slow

Skip et Todo

it('is not ready yet', function () { // Test à implémenter })->todo(); it('runs only on local', function () { // Test })->skip(fn () => app()->environment('production')); it('requires extension', function () { // Test })->skipOnPhp('< 8.1');

🚀 Exécution

# Lancer tous les tests pest php artisan test # Coverage pest --coverage pest --coverage --min=80 # Parallèles pest --parallel # Watch mode pest --watch # Fichier spécifique pest tests/Feature/PostTest.php # Filter pest --filter="can create post" # Output pest -v # verbose pest --compact # CI pest --list-tests # liste sans exécuter

🎨 Arch Testing

// tests/Arch.php arch('controllers') ->expect('App\Http\Controllers') ->toExtend('App\Http\Controllers\Controller') ->toHaveSuffix('Controller') ->not->toBeUsed(); arch('models') ->expect('App\Models') ->toExtend('Illuminate\Database\Eloquent\Model') ->toOnlyBeUsedIn('App\Http\Controllers'); arch('no debugging') ->expect(['dd', 'dump', 'var_dump']) ->not->toBeUsed(); arch('strict types') ->expect('App') ->toUseStrictTypes();

💡 Best Practices

Structure claire

it('creates and updates a post', function () { // Arrange $user = User::factory()->create(); $this->actingAs($user); // Act $post = Post::create([ 'title' => 'Test', 'content' => 'Content', 'user_id' => $user->id, ]); // Assert expect($post) ->title->toBe('Test') ->and($post->user_id)->toBe($user->id); });

Tests lisibles

// ❌ Mauvais test('t1', fn () => expect(u())->i(U::class)); // ✅ Bon it('creates a user with valid data', function () { $user = User::factory()->create(); expect($user)->toBeInstanceOf(User::class); });

Les tests deviennent un plaisir, pas une corvée