Kiểm thử tự động không nhằm chứng minh phần mềm không có lỗi; nó tạo ra tín hiệu sớm khi hành vi quan trọng bị thay đổi ngoài ý muốn. Với Laravel và Pest, nhóm có thể bắt đầu từ các luồng nghiệp vụ nhỏ, viết test dễ đọc và dần xây dựng mạng lưới bảo vệ cho những phần rủi ro nhất của hệ thống.
Nên bắt đầu từ đâu?
Đừng đặt mục tiêu phủ toàn bộ code ngay lập tức. Hãy ưu tiên những hành vi nếu hỏng sẽ gây thiệt hại rõ ràng: đăng nhập, phân quyền, tạo đơn hàng, thanh toán, cập nhật tồn kho, API tích hợp và các lỗi từng xuất hiện trên production.
| Loại test | Phù hợp với | Đặc điểm |
|---|---|---|
| Unit | Hàm tính toán, quy tắc nghiệp vụ độc lập | Chạy nhanh, ít phụ thuộc framework |
| Feature | HTTP, database, middleware, quyền truy cập | Cho tín hiệu gần với hành vi thực tế |
| Browser | Luồng giao diện quan trọng | Giá trị cao nhưng chậm và cần chăm sóc nhiều hơn |
| Architecture | Quy tắc cấu trúc và dependency | Ngăn codebase lệch khỏi convention |
Test đầu tiên bằng Pest
<?php
it('redirects guests away from the dashboard', function () {
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
});
Tên test nên diễn đạt kết quả quan sát được. Tránh đặt tên theo method nội bộ, vì refactor có thể đổi method nhưng hành vi người dùng vẫn giữ nguyên.
Kiểm tra một luồng có database
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('allows an active user to update their profile', function () {
$user = User::factory()->create(['is_active' => true]);
$response = $this->actingAs($user)->put('/profile', [
'name' => 'Nguyen Minh An',
'email' => 'an@example.test',
]);
$response->assertRedirect('/profile');
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Nguyen Minh An',
]);
});
RefreshDatabase giúp mỗi test bắt đầu từ trạng thái dữ liệu kiểm soát được. Factory tạo bản ghi rõ ý nghĩa hơn việc phụ thuộc vào database có sẵn trên máy người viết test.
Test cả đường thành công và đường từ chối
Chỉ kiểm tra trường hợp hợp lệ dễ bỏ sót lỗ hổng. Với endpoint cập nhật hồ sơ, nên thêm các tình huống như người dùng chưa đăng nhập, email sai định dạng, email trùng và tài khoản bị khóa. Với phân quyền, hãy kiểm tra cả người có quyền và người không có quyền.
Test giá trị cao bảo vệ một cam kết nghiệp vụ. Nó không cần biết controller gọi bao nhiêu method; nó cần chứng minh người dùng đúng vai trò nhận được đúng kết quả.
Giữ test ổn định và dễ bảo trì
- Dùng factory state như
active()hoặcadmin()để dữ liệu thể hiện rõ ý định. - Không gọi API bên thứ ba thật; dùng fake cho mail, queue, notification, storage và HTTP client.
- Đóng băng thời gian khi logic liên quan đến hạn dùng hoặc lịch chạy.
- Không phụ thuộc thứ tự test; mỗi test phải tự chuẩn bị trạng thái.
- Ưu tiên assertion về kết quả nghiệp vụ, hạn chế khóa test vào HTML hoặc cấu trúc nội bộ.
Fake các tác vụ phụ
use Illuminate\Support\Facades\Queue;
use App\Jobs\SendWelcomeEmail;
it('queues a welcome email after registration', function () {
Queue::fake();
$this->post('/register', [
'name' => 'Lan',
'email' => 'lan@example.test',
'password' => 'Strong-password-123',
'password_confirmation' => 'Strong-password-123',
])->assertRedirect('/dashboard');
Queue::assertPushed(SendWelcomeEmail::class);
});
Chạy nhanh trên máy lập trình
php artisan test
php artisan test --stop-on-failure
php artisan test --filter=profile
php artisan test --parallel --processes=4
Chạy song song có thể rút ngắn thời gian, nhưng test phải cô lập tài nguyên. Tệp dùng chung, cổng mạng cố định hoặc dịch vụ bên ngoài có thể gây lỗi ngẫu nhiên giữa các process.
Đưa test vào CI
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-interaction --prefer-dist
- run: cp .env.example .env
- run: php artisan key:generate
- run: php artisan test
Đây là khung khởi đầu. Dự án dùng MySQL, Redis hoặc trình duyệt cần thêm service và cấu hình tương ứng. Không dùng thông tin bí mật production cho CI và phải tách riêng database test.
Dấu hiệu test đang thành gánh nặng
- Test thất bại ngẫu nhiên khi code không đổi.
- Một thay đổi giao diện nhỏ làm hỏng nhiều test không liên quan.
- Fixture quá lớn khiến người đọc không biết dữ liệu nào quan trọng.
- Test mock mọi lớp trung gian và chỉ xác nhận cách triển khai hiện tại.
- Bộ test chậm đến mức nhóm bỏ qua trước khi push.
Khi thấy các dấu hiệu này, hãy giảm dữ liệu, cô lập dịch vụ ngoài, chia test theo mức độ và giữ một nhóm smoke test chạy nhanh. Xóa test trùng lặp cũng là bảo trì có giá trị.
Lộ trình áp dụng thực tế
- Chọn ba đến năm luồng kinh doanh quan trọng nhất.
- Viết feature test cho thành công, validation và phân quyền.
- Thêm regression test mỗi khi sửa một lỗi production.
- Chạy nhóm test nhanh trên mọi pull request.
- Theo dõi thời gian chạy và lỗi không ổn định.
- Mở rộng sang browser test hoặc architecture test khi giá trị đã rõ.
Kết luận
Một bộ test hữu ích không được đo chỉ bằng phần trăm coverage. Nó được đo bằng khả năng phát hiện thay đổi nguy hiểm sớm, giải thích lỗi rõ và chạy đủ nhanh để đội ngũ sử dụng thường xuyên. Với Laravel và Pest, hãy bắt đầu bằng hành vi quan trọng, quản lý dữ liệu chặt chẽ và để CI biến các cam kết đó thành bước kiểm tra lặp lại được.
Nguồn tham khảo
Bài 30 TaskFlow: đưa Pest vào bộ test đang có
Các ví dụ profile/is_active/đăng ký/SendWelcomeEmail phía trên là minh họa cho ứng dụng có những chức năng đó, không phải endpoint sẵn có của TaskFlow. Dự án khóa học hiện chưa có sửa profile hay đăng ký công khai. Phần thực hành dưới đây dùng đúng CreateTaskData, ProjectPolicy và route projects.show đã triển khai.
Chọn phiên bản và giữ test cũ
TaskFlow giữ nhánh Pest 4 cùng PHPUnit 12; không hiểu lệnh composer require không ràng buộc là luôn cài phiên bản phù hợp. Tại checkpoint, Composer giải ra Pest 4.7.8, plugin Laravel 4.1.0 và PHPUnit 12.5.33; PHPUnit giảm patch từ 12.5.35 theo ràng buộc của Pest. Toàn bộ regression suite phải chạy lại sau thay đổi này. Runtime kiểm chứng là PHP 8.4.25; dùng composer check-platform-reqs để kiểm tra chính lockfile, không chỉ dựa vào yêu cầu tối thiểu của Laravel.
Đọc dry-run, sau đó chạy lại lệnh bỏ --dry-run nếu thay đổi phù hợp; commit composer.json và composer.lock. Với bản dự án đã có lock, chỉ chạy composer install. Không xóa các class PHPUnit cũ: Pest chạy được chúng cùng các test dạng closure. Không chạy pest --init ghi đè cấu hình đã chuẩn bị. Tách lệnh shell và nội dung tests/Pest.php trong ví dụ dưới đây thành đúng vị trí.
composer require --dev "pestphp/pest:^4.0" "pestphp/pest-plugin-laravel:^4.0" --with-all-dependencies --dry-run
# Inspect the proposed dependency changes before removing --dry-run.
// tests/Pest.php
<?php
pest()->extend(Tests\TestCase::class)->in('Feature/Pest');Chỉ các closure trong Feature/Pest mở rộng Tests\TestCase, nên mới có ứng dụng Laravel, $this->get và container. Unit test không tự boot framework. Không thêm RefreshDatabase toàn cục: bộ test queue/transaction hiện có chủ động kiểm tra commit thật; bọc chúng trong transaction của trait sẽ thay đổi hành vi đang đo.
Dataset cho quy tắc đầu vào
Mỗi dataset có tên riêng để output nói rõ trường hợp nào lỗi. Closure truyền vào expect trì hoãn việc gọi constructor để assertion bắt exception; nếu khởi tạo trước expect thì exception thoát ra ngoài assertion. Kiểm tra cả 120 ký tự hợp lệ và 121 ký tự bị từ chối để bảo vệ ranh giới, không chỉ một chuỗi quá dài bất kỳ.
<?php
// tests/Unit/CreateTaskDataPestTest.php
use App\Data\CreateTaskData;
it('rejects invalid task input before persistence', function (string $title, string $priority) {
expect(fn () => new CreateTaskData($title, priority: $priority))
->toThrow(InvalidArgumentException::class);
})->with([
'blank title' => [' ', 'normal'],
'long title' => [str_repeat('a', 121), 'normal'],
'unknown priority' => ['Task', 'urgent'],
]);
it('accepts a task title exactly at the length limit', function () {
$data = new CreateTaskData(str_repeat('a', 120), priority: 'high');
expect($data->title)->toHaveLength(120)->and($data->priority)->toBe('high');
});Feature test với quyền truy cập thật
Test sau dùng HTTP kernel, factory và policy thật, không mock ProjectPolicy. beforeEach ép SQLite :memory:, bỏ URL cấu hình có thể ưu tiên hơn tên database, purge connection cũ rồi migrate. Vì database chỉ tồn tại trong bộ nhớ của test, bước migrate này không đụng file database phát triển. Không lấy pattern migrate --force này chạy trên connection chưa xác nhận.
<?php
// tests/Feature/Pest/ProjectAccessTest.php
use App\Models\Project;
use App\Models\User;
use Illuminate\Support\Facades\DB;
beforeEach(function () {
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:',
'database.connections.sqlite.url' => null]);
DB::purge('sqlite');
$this->artisan('migrate', ['--force' => true])->assertSuccessful();
});
it('redirects guests away from the dashboard', function () {
$this->get('/dashboard')->assertRedirect('/login');
});
it('allows only the project owner to open a project', function (bool $isOwner, int $status) {
$project = Project::factory()->create();
$user = $isOwner ? $project->owner : User::factory()->create();
$this->actingAs($user)->get(route('projects.show', $project))->assertStatus($status);
})->with(['owner' => [true, 200], 'unrelated user' => [false, 403]]);Dataset owner mong đợi 200, người không liên quan mong đợi 403; guest vào dashboard phải redirect login. actingAs chỉ thiết lập danh tính cho test, không kiểm tra form đăng nhập/mật khẩu. Feature test thông thường cũng bỏ qua CSRF; test CSRF riêng của TaskFlow cần bật lại hành vi middleware. Không diễn giải ba assertion này thành bằng chứng toàn bộ bảo mật đã đạt.
Chạy và đọc kết quả
php artisan test --filter=CreateTaskDataPestTest
php artisan test --filter=ProjectAccessTest
php artisan test
composer check-platform-reqs
Kết quả thực tế: Pest chạy 94 tests, 421 assertions, tất cả pass; gồm 87 test cũ và 7 trường hợp Pest mới. Đây là số kiểm thử tại checkpoint, không phải mức coverage. Chưa chạy mutation, coverage, browser hay parallel cho phần này. Để tự đánh giá độ nhạy, trên nhánh thử nghiệm hãy đổi giới hạn DTO hoặc bỏ điều kiện owner, kiểm tra test tương ứng đỏ rồi phục hồi thay đổi; không đưa lỗi thử nghiệm lên production.
CI ở phần tổng quan là khung minh họa, không chứng minh pipeline của TaskFlow đã chạy. Bài 32 sẽ bổ sung cấu hình CI theo lockfile và runtime của dự án. Bài 31 trước đó đi sâu vào database, auth, policy và queue, đặc biệt phân biệt fake dispatch với worker thực thi thật.
Tham khảo phiên bản: Pest 4 installation. Điều hướng: Bài 29: Reverb · Lộ trình.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.