Viết test cho Laravel với PHPUnit và Pest: bắt đầu từ đâu?

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Sửa một dòng code mà không dám deploy vì sợ hỏng chỗ khác? Test tự động chính là tấm lưới an toàn cho nỗi sợ đó. Laravel hỗ trợ kiểm thử rất tốt ngay từ khi cài đặt, bài viết này chỉ bạn cách bắt đầu một cách thực tế.

Feature test và Unit test

Laravel chia test thành hai loại chính:

  • Unit test: kiểm tra một đơn vị nhỏ (một hàm, một class) độc lập, thường không cần database hay HTTP.
  • Feature test: kiểm tra một tính năng đi qua nhiều lớp, ví dụ gọi endpoint và xem kết quả, có thể chạm database.

Với ứng dụng web, feature test mang lại giá trị lớn nhất cho công sức bỏ ra vì chúng giống cách người dùng thật sự dùng sản phẩm.

Test đầu tiên với PHPUnit

Tạo test bằng php artisan make:test PostApiTest, rồi viết:

use Illuminate\Foundation\Testing\RefreshDatabase;class PostApiTest extends TestCase{    use RefreshDatabase;    public function test_user_can_create_post(): void    {        $user = User::factory()->create();        $response = $this->actingAs($user)->postJson('/api/posts', [            'title' => 'Bài đầu tiên',            'body'  => 'Nội dung bài viết đủ dài để hợp lệ.',        ]);        $response->assertCreated();        $this->assertDatabaseHas('posts', ['title' => 'Bài đầu tiên']);    }    public function test_guest_cannot_create_post(): void    {        $this->postJson('/api/posts', [])->assertUnauthorized();    }}

Trait RefreshDatabase đưa database về trạng thái sạch cho mỗi test, nên các test không ảnh hưởng nhau. Chạy bằng php artisan test.

Cùng bài test bằng Pest

Pest là bộ khung kiểm thử xây trên PHPUnit với cú pháp gọn và dễ đọc:

use function Pest\Laravel\{actingAs, postJson};it('lets a user create a post', function () {    $user = User::factory()->create();    actingAs($user);    postJson('/api/posts', ['title' => 'Bài đầu tiên', 'body' => 'Nội dung đủ dài để hợp lệ.'])        ->assertCreated();});it('rejects guests', function () {    postJson('/api/posts', [])->assertUnauthorized();});

Cả hai đều chạy được trong cùng dự án, bạn chọn phong cách mình thích. Kiến thức về assertion và factory dùng chung cho cả hai.

Factory và dữ liệu giả

Factory sinh dữ liệu mẫu để test không phải tạo thủ công từng trường:

Post::factory()->count(3)->for($user)->create();

Với dịch vụ bên ngoài như gửi mail hay gọi API thanh toán, hãy dùng fake có sẵn: Mail::fake(), Queue::fake(), Http::fake(). Test sẽ nhanh, ổn định và không gửi thư thật ra ngoài.

Mail::fake();// ... thực hiện hành độngMail::assertSent(WelcomeMail::class);

Nên test cái gì trước?

  1. Luồng quan trọng với doanh thu: đăng ký, đăng nhập, thanh toán.
  2. Phân quyền: người này không được xem dữ liệu của người kia.
  3. Các lỗi từng xảy ra: mỗi khi sửa bug, viết một test tái hiện nó để không bị lại.
  4. Logic tính toán phức tạp có nhiều nhánh.

Đừng đặt mục tiêu phủ 100% ngay. Một bộ test nhỏ nhưng chạy ổn định mỗi lần commit còn tốt hơn một bộ đồ sộ mà không ai dám chạy.

Tích hợp vào quy trình

Test chỉ có giá trị nếu được chạy thường xuyên. Hãy chạy chúng tự động trên CI mỗi khi có pull request, và coi test đỏ là lý do dừng merge. Dùng database SQLite trong bộ nhớ hoặc một database riêng cho test để chạy nhanh và an toàn.

Những thói quen giúp test bền vững

Test tốt cần đọc như một mô tả hành vi. Hãy đặt tên theo điều mong đợi, ví dụ test_guest_cannot_create_post, để khi test lỗi bạn hiểu ngay thứ gì đang hỏng. Mỗi test chỉ nên kiểm tra một khía cạnh, chuẩn bị dữ liệu bằng factory thay vì phụ thuộc dữ liệu có sẵn, và không để test này phụ thuộc kết quả của test khác. Tránh kiểm tra chi tiết cài đặt bên trong; hãy kiểm tra kết quả nhìn thấy được như mã trạng thái, nội dung JSON và dữ liệu đã lưu.

Cuối cùng, hãy giữ cho bộ test chạy nhanh. Nếu mất mười phút mới có kết quả, mọi người sẽ ngừng chạy nó. Tách các test chậm (gọi dịch vụ thật, trình duyệt) khỏi nhóm test chạy hằng ngày, và dùng php artisan test --parallel để tận dụng nhiều nhân CPU khi bộ test lớn dần.

Test cho API có xác thực

Khi API dùng token, bạn không cần đi qua bước đăng nhập trong mỗi test. Hãy dùng actingAs($user, 'sanctum') để giả lập người dùng đã xác thực, và dành riêng một vài test cho chính luồng đăng nhập. Cách chia này giữ cho test nhanh mà vẫn bao phủ các điểm quan trọng. Với các endpoint trả về danh sách, nên kiểm tra cả cấu trúc JSON bằng assertJsonStructure để chắc chắn ứng dụng khách nhận đúng các trường mà nó cần.

Kết luận

Bắt đầu nhỏ: một test cho luồng đăng nhập, một test cho phân quyền, rồi tăng dần. Sau vài tuần, bạn sẽ thấy việc sửa code bớt đáng sợ hơn hẳn, và đó là phần thưởng lớn nhất của kiểm thử tự động.

Muốn học bài bản và có lộ trình rõ ràng? Bạn có thể xem các khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment