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

  1. One concept per test - Test một điều duy nhất
  2. Use descriptive names - Nên giải thích cái được test
  3. Keep tests DRY - Sử dụng datasets và hooks
  4. Test behavior, not implementation - Tập trung vào outputs
  5. Use factories - Cho realistic test data
  6. Mock external services - Không gọi real APIs
  7. 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.

Bình luận