Pest PHP: Framework Testing Hiện đại cho Laravel
·
6 min read
Pest là một PHP testing framework thanh lịch giúp việc viết tests cảm giác ít như công việc hơn. Xây dựng trên PHPUnit, nó cung cấp một syntax expressive hơn và các tính năng mạnh mẽ trong khi duy trì khả năng tương thích với các tests hiện có.
Installation
composer require pestphp/pest pestphp/pest-plugin-laravel --dev
php artisan pest:install
Điều này tạo ra cấu trúc thư mục tests/ và file cấu hình Pest.php.
Cấu trúc Test Cơ bản
Test đơn giản nhất:
// tests/Feature/HomeTest.php
test('homepage loads successfully', function () {
$response = $this->get('/');
$response->assertStatus(200);
});
So sánh với PHPUnit:
public function test_homepage_loads_successfully()
{
$response = $this->get('/');
$this->assertEquals(200, $response->getStatusCode());
}
Sạch hơn nhiều!
Mô tả Tests với Groups
describe('Authentication', function () {
test('user can register', function () {
$response = $this->post('/register', [
'name' => 'John',
'email' => 'john@example.com',
'password' => 'password123',
]);
$response->assertRedirect('/dashboard');
});
test('user can login', function () {
$this->post('/login', [
'email' => 'john@example.com',
'password' => 'password123',
])->assertRedirect('/dashboard');
});
});
Tạo các nhóm logic trong output test của bạn.
Sử dụng Datasets
Test nhiều kịch bản với một test function duy nhất:
test('can calculate discount', function (int $price, int $discount, int $expected) {
$result = calculateDiscount($price, $discount);
expect($result)->toBe($expected);
})
->with([
[100, 10, 90],
[200, 20, 160],
[50, 5, 47.5],
]);
Hoặc sử dụng named datasets:
test('validates email format', function (string $email, bool $valid) {
expect(isValidEmail($email))->toBe($valid);
})
->with('emails');
dataset('emails', [
'valid email' => ['john@example.com', true],
'invalid email' => ['not-an-email', false],
'empty email' => ['', false],
]);
Hooks và Setup
beforeEach(function () {
// Chạy trước mỗi test trong file này
$this->user = User::factory()->create();
});
afterEach(function () {
// Chạy sau mỗi test
Log::flushEvents();
});
test('user can create post', function () {
$post = $this->user->posts()->create([
'title' => 'My First Post',
]);
expect($post)->toBeInstanceOf(Post::class);
});
Higher-Order Tests
Chain assertions cho readable test flows:
test('user can delete their own post')
->actingAs(User::factory()->create())
->post('/posts', [
'title' => 'My Post',
'body' => 'Content here',
])
->assertRedirect()
->delete('/posts/1')
->assertRedirect('/posts');
Testing Database State
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('user can create post', function () {
$post = Post::create([
'title' => 'Test',
'body' => 'Content',
]);
expect($post)->toBeInstanceOf(Post::class);
$this->assertDatabaseHas('posts', [
'title' => 'Test',
]);
});
API Testing
test('api returns user data', function () {
$user = User::factory()->create();
$this->getJson('/api/users/' . $user->id)
->assertOk()
->assertJsonPath('data.name', $user->name)
->assertJsonStructure([
'data' => [
'id',
'name',
'email',
]
]);
});
Testing Exceptions
use Pest\Exceptions\ToThrowException;
test('throws when user not found', function () {
expect(function () {
User::findOrFail(999);
})->toThrow(ModelNotFoundException::class);
});
test('throws with specific message', function () {
expect(function () {
throw new Exception('Custom error');
})->toThrow(Exception::class, 'Custom error');
});
Sử dụng Expectations
test('user email is correct', function () {
$user = User::factory()->create(['email' => 'john@example.com']);
expect($user->email)->toBe('john@example.com');
expect($user->email)->toContain('@example.com');
expect($user->email)->toMatch('/^[^@]+@[^@]+\.[^@]+$/');
});
test('collection has items', function () {
$users = User::factory(5)->create();
expect($users)->toHaveCount(5);
expect($users)->each->toBeInstanceOf(User::class);
});
Mocking
use Illuminate\Support\Facades\Http;
test('fetches data from external api', function () {
Http::fake([
'api.example.com/users/*' => Http::response([
'id' => 1,
'name' => 'John',
], 200),
]);
$response = Http::get('https://api.example.com/users/1');
expect($response->json())
->toHaveKey('name', 'John');
});
Event Testing
use Illuminate\Support\Facades\Event;
test('post created event fires', function () {
Event::fake();
Post::factory()->create();
Event::assertDispatched(PostCreated::class);
});
Testing Queries
test('eager loads author', function () {
Post::factory()
->has(Comment::factory(5))
->create();
$this->assertQueryCount(1, function () {
$post = Post::with('comments')->first();
$post->comments->count();
});
});
Tổ chức Tests
Cấu trúc tiêu chuẩn:
tests/
├── Feature/ # Integration tests
│ ├── HomeTest.php
│ ├── PostTest.php
├── Unit/ # Unit tests
│ ├── CalculatorTest.php
└── Pest.php # Configuration
Chạy Tests
# Chạy tất cả tests
php artisan test
# Chạy file cụ thể
php artisan test tests/Feature/PostTest.php
# Chạy tests matching pattern
php artisan test --name="post"
# Chạy với coverage
php artisan test --coverage
# Chạy song song
php artisan test --parallel
Configuration (tests/Pest.php)
uses(
Tests\TestCase::class,
RefreshDatabase::class,
)->in('Feature');
uses(Tests\TestCase::class)->in('Unit');
// Global expectations
expect()->extend('toBeActive', function () {
return $this->toBe(true);
});
Snapshot Testing
test('api response matches snapshot', function () {
$response = $this->getJson('/api/users/1');
expect($response->json())->toMatchSnapshot();
});
Lần chạy đầu tiên tạo snapshot, các lần chạy tiếp theo xác minh lại nó.
Best Practices
- One concept per test - Test một điều duy nhất
- Use descriptive names - Nên giải thích cái được test
- Keep tests DRY - Sử dụng datasets và hooks
- Test behavior, not implementation - Tập trung vào outputs
- Use factories - Cho realistic test data
- Mock external services - Không gọi real APIs
- Arrange-Act-Assert - Clear test structure
test('user can update profile', function () {
// Arrange
$user = User::factory()->create();
// Act
$this->actingAs($user)
->put("/users/{$user->id}", [
'name' => 'Jane',
]);
// Assert
expect($user->fresh()->name)->toBe('Jane');
});
Tóm tắt
Pest làm cho testing thú vị với syntax sạch, expressive của nó. Các lợi ích chính:
- Readable - Tests đọc như documentation
- Concise - Ít boilerplate hơn PHPUnit
- Powerful - Datasets, hooks, và higher-order tests
- Compatible - Hoạt động bên cạnh PHPUnit
- Developer-friendly - Khuyến khích better testing practices
Bắt đầu với Pest và không bao giờ quay lại.