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/pestStructure
tests/
├── Feature/ # Tests fonctionnels (HTTP)
├── Unit/ # Tests unitaires (logique isolée)
├── Pest.php # Configuration globale
└── TestCase.php # Classe de baseConfiguration 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 --pestSyntaxe 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=slowSkip 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