NTM Solutions

Chủ Nhật, 27 tháng 9, 2026

🚀LARAVEL 13 — BONUS — Kiểm thử với PHPUnit

Trong 50 bài chính, chúng ta đã hoàn thành một Blog CMS với:

  • Authentication

  • CRUD

  • Database

  • Eloquent

  • Relationship

  • Validation

  • Upload

  • Search

  • Pagination

  • Soft Delete

  • Authorization

  • Queue

  • Mail

  • Notification

  • Cache

  • Storage

  • Logging

  • REST API

  • Sanctum

  • Deployment

Nhưng có một vấn đề:

Làm sao biết code của chúng ta vẫn hoạt động đúng sau khi sửa hoặc thêm tính năng mới?

Đó là lúc Automated Testing xuất hiện.

Trong bài Bonus này, chúng ta chỉ học những kiến thức PHPUnit cần thiết để bắt đầu kiểm thử một ứng dụng Laravel 13.


1. Testing là gì?

Testing là quá trình kiểm tra xem chương trình có hoạt động đúng theo yêu cầu hay không.

Ví dụ Blog CMS có chức năng:

GET /blog

Chúng ta mong muốn:

HTTP 200

Thay vì mỗi lần sửa code lại mở trình duyệt kiểm tra bằng tay, chúng ta có thể viết test:

$response = $this->get('/blog');

$response->assertOk();

Sau đó Laravel tự kiểm tra.

Nếu đúng:

PASS

Nếu sai:

FAIL

2. PHPUnit là gì?

PHPUnit là framework testing phổ biến trong PHP.

Laravel tích hợp sẵn khả năng testing với PHPUnit và Pest. Một project Laravel mới đã có phpunit.xml và thư mục tests.

Trong bài này chúng ta tập trung vào:

PHPUnit

vì mục tiêu của Bonus là giúp người mới hiểu nền tảng testing.


3. Tests nằm ở đâu?

Trong Laravel:

tests/
│
├── Feature/
│
├── Unit/
│
└── TestCase.php

Hai loại quan trọng:

Unit Test
Feature Test

4. Unit Test

Unit Test kiểm tra một phần nhỏ và tương đối độc lập của code.

Ví dụ:

Một method
Một class
Một hàm xử lý
Một business rule

Ví dụ:

public function add(int $a, int $b): int
{
    return $a + $b;
}

Test:

$this->assertEquals(
    5,
    $calculator->add(2, 3)
);

Unit Test thường không cần boot toàn bộ ứng dụng Laravel hoặc truy cập database.


5. Feature Test

Feature Test kiểm tra một chức năng lớn hơn.

Ví dụ:

Browser
   ↓
Route
   ↓
Middleware
   ↓
Controller
   ↓
Model
   ↓
Database
   ↓
Response

Ví dụ:

$response = $this->get('/blog');

$response->assertOk();

Đây là một Feature Test.

Laravel lưu ý rằng Feature Test có thể kiểm tra cả HTTP request, database và nhiều thành phần phối hợp với nhau; trong ứng dụng thực tế, phần lớn test thường có thể tập trung vào Feature Test vì nó cho mức độ tin cậy cao hơn về hành vi tổng thể của hệ thống.


6. Tạo Test

Laravel cung cấp Artisan command:

php artisan make:test BlogTest

File sẽ nằm trong:

tests/Feature/BlogTest.php

Nếu muốn tạo Unit Test:

php artisan make:test CalculatorTest --unit

File:

tests/Unit/CalculatorTest.php

7. Test đầu tiên

Tạo:

php artisan make:test BlogTest

Sau đó mở:

tests/Feature/BlogTest.php

Ví dụ:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class BlogTest extends TestCase
{
    public function test_blog_page_is_accessible(): void
    {
        $response = $this->get('/blog');

        $response->assertOk();
    }
}

Test này nói:

Khi request /blog, ứng dụng phải trả HTTP 200.


8. Chạy Test

Có thể chạy:

php artisan test

Laravel cũng cho phép chạy trực tiếp PHPUnit:

vendor/bin/phpunit

Laravel cung cấp php artisan test như một cách thuận tiện để chạy test suite.

Kết quả có thể giống:

PASS  Tests\Feature\BlogTest
✓ blog page is accessible

Tests:    1 passed

Nếu test thất bại:

FAIL

Laravel sẽ hiển thị thông tin để chúng ta tìm nguyên nhân.


9. assertOk()

Một assertion rất thường dùng:

$response->assertOk();

Nó kiểm tra HTTP response có status:

200 OK

Ví dụ:

public function test_blog_page_is_accessible(): void
{
    $response = $this->get('/blog');

    $response->assertOk();
}

10. assertStatus()

Có thể kiểm tra status cụ thể:

$response->assertStatus(200);

Ví dụ:

$response = $this->get('/blog');

$response->assertStatus(200);

Hoặc:

$response->assertStatus(404);

Ví dụ test một trang không tồn tại:

public function test_missing_page_returns_404(): void
{
    $response = $this->get('/this-page-does-not-exist');

    $response->assertStatus(404);
}

11. assertRedirect()

Nếu request phải redirect:

$response->assertRedirect('/login');

Ví dụ:

public function test_admin_page_requires_login(): void
{
    $response = $this->get('/admin/users');

    $response->assertRedirect('/login');
}

Đây là một test rất thực tế đối với Blog CMS.


12. Kiểm tra nội dung HTML

Có thể kiểm tra response có chứa text:

$response->assertSee('Blog');

Ví dụ:

public function test_blog_contains_title(): void
{
    $response = $this->get('/blog');

    $response->assertOk();
    $response->assertSee('Blog');
}

Hoặc:

$response->assertDontSee('Something');

13. Test JSON API

Đây là phần đặc biệt quan trọng đối với Blog CMS.

API:

GET /api/posts

Test:

public function test_posts_api_returns_success(): void
{
    $response = $this->getJson('/api/posts');

    $response->assertStatus(200);
}

Hoặc:

$response->assertOk();

14. assertJson()

Giả sử API trả về:

{
    "data": [
        {
            "id": 1,
            "title": "Laravel 13"
        }
    ]
}

Có thể kiểm tra:

$response->assertJson([
    'data' => [
        [
            'id' => 1,
            'title' => 'Laravel 13',
        ],
    ],
]);

Mục tiêu là kiểm tra API trả về đúng cấu trúc dữ liệu mong muốn.


15. Test Database

Feature Test có thể kiểm tra database.

Laravel cung cấp các trait hỗ trợ việc reset database giữa các test.

Ví dụ:

use Illuminate\Foundation\Testing\RefreshDatabase;

Sau đó:

class PostTest extends TestCase
{
    use RefreshDatabase;

    // ...
}

Mỗi test có thể làm việc với database testing thay vì phá dữ liệu development hiện tại.


16. Tạo dữ liệu bằng Factory

Giả sử chúng ta có:

PostFactory

Có thể tạo Post:

$post = Post::factory()->create();

Sau đó test:

$response = $this->get('/blog/' . $post->slug);

$response->assertOk();

Đây là cách rất phù hợp với Blog CMS.


17. Test Post

Ví dụ:

<?php

namespace Tests\Feature;

use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_post_page_can_be_opened(): void
    {
        $post = Post::factory()->create();

        $response = $this->get(
            '/blog/' . $post->slug
        );

        $response->assertOk();
    }
}

Flow:

Factory
   ↓
Create Post
   ↓
Request URL
   ↓
Laravel
   ↓
Response
   ↓
assertOk()

18. Test Authentication

Ví dụ một trang admin yêu cầu đăng nhập.

public function test_admin_requires_authentication(): void
{
    $response = $this->get('/admin/users');

    $response->assertRedirect('/login');
}

Sau đó test user đã đăng nhập:

$user = User::factory()->create();

$this->actingAs($user);

$response = $this->get('/admin/users');

Đây là cách kiểm tra các route có authentication.


19. Test Authorization

Authentication chưa đủ.

Chúng ta còn phải kiểm tra quyền.

Ví dụ:

user
admin

Một user thông thường không được phép truy cập:

/admin/users

Test có thể kiểm tra:

$user = User::factory()->create([
    'role' => 'user',
]);

$this->actingAs($user);

$response = $this->get('/admin/users');

$response->assertForbidden();

Trong trường hợp ứng dụng của bạn trả về redirect hoặc response khác tùy middleware/policy, assertion phải phù hợp với implementation thực tế.

Điểm quan trọng:

Phải test cả Authentication và Authorization.


20. Test Validation

Ví dụ tạo Post yêu cầu:

title
content
category_id

Nếu gửi request thiếu title:

$response = $this->post('/admin/posts', [
    'content' => 'Test content',
]);

Có thể kiểm tra validation:

$response->assertSessionHasErrors([
    'title',
]);

Đây là một test rất hữu ích.


21. Test REST API

Blog CMS của chúng ta có:

GET
POST
PUT/PATCH
DELETE

Do đó có thể xây dựng một bộ test:

Post API
│
├── GET /api/posts
├── GET /api/posts/{id}
├── POST /api/posts
├── PUT /api/posts/{id}
└── DELETE /api/posts/{id}

Ví dụ:

public function test_posts_api_returns_json(): void
{
    $response = $this->getJson('/api/posts');

    $response->assertOk();

    $response->assertJsonStructure([
        'data',
    ]);
}

22. Test Sanctum API

API private yêu cầu:

Bearer Token

Có thể tạo user:

$user = User::factory()->create();

Tạo token:

$token = $user->createToken('testing')->plainTextToken;

Sau đó request:

$response = $this
    ->withToken($token)
    ->getJson('/api/user');

Kiểm tra:

$response->assertOk();

23. Test API Authentication

Test request không có token:

public function test_private_api_requires_authentication(): void
{
    $response = $this->getJson('/api/user');

    $response->assertUnauthorized();
}

Kết quả mong muốn:

HTTP 401

Sau đó test với token hợp lệ:

$user = User::factory()->create();

$token = $user
    ->createToken('testing')
    ->plainTextToken;

$response = $this
    ->withToken($token)
    ->getJson('/api/user');

$response->assertOk();

24. Test Event

Trong Blog CMS, chúng ta đã học Event và Listener.

Testing cũng có thể kiểm tra Event có được dispatch hay không.

Laravel cung cấp:

Event::fake();

Sau đó chạy code cần kiểm tra.

Cuối cùng:

Event::assertDispatched(
    Registered::class
);

Laravel hỗ trợ Event::fake() và các assertion như assertDispatched() để kiểm tra event mà không cần thực thi listener trong test đó.

Flow:

Test
 │
 ▼
Event::fake()
 │
 ▼
Execute Code
 │
 ▼
Event dispatched?
 │
 ├── YES → PASS
 └── NO  → FAIL

25. Test Queue

Tương tự Event, Queue cũng có thể được fake.

Ví dụ:

Queue::fake();

Sau đó:

SomeJob::dispatch();

Kiểm tra:

Queue::assertPushed(SomeJob::class);

Mục tiêu:

Kiểm tra Job đã được dispatch hay chưa.

Không nhất thiết phải thực sự chạy Job trong test này.


26. Test Mail

Mail cũng có thể fake.

Ví dụ:

Mail::fake();

Sau đó:

Mail::to($user)->send(
    new WelcomeMail($user)
);

Kiểm tra:

Mail::assertSent(WelcomeMail::class);

Điều này đặc biệt hữu ích với:

Register
Password Reset
Contact Form
Notification
Order Confirmation

27. Test Upload

Blog CMS có upload image.

Có thể fake Storage:

Storage::fake('public');

Sau đó:

$file = UploadedFile::fake()
    ->image('post.jpg');

Gửi request:

$response = $this->post('/admin/posts', [
    'title' => 'Laravel 13',
    'image' => $file,
]);

Kiểm tra file:

Storage::disk('public')->assertExists(
    'posts/' . $file->hashName()
);

Đây là cách test upload mà không cần ghi file thật vào storage production.


28. Test Soft Delete

Blog CMS có Soft Delete.

Có thể kiểm tra:

$post = Post::factory()->create();

$post->delete();

Sau đó:

$this->assertSoftDeleted($post);

Hoặc kiểm tra record đã bị đánh dấu:

deleted_at != NULL

Sau đó test restore:

$post->restore();

29. Test một CRUD hoàn chỉnh

Ví dụ Post:

Create
Read
Update
Delete

Chúng ta có thể xây dựng:

PostTest
│
├── can list posts
├── can show post
├── can create post
├── can update post
├── can delete post
└── can restore post

Đây chính là cách biến test thành một bộ kiểm tra tự động cho CMS.


30. Ví dụ Feature Test cho Blog CMS

Một test đơn giản:

<?php

namespace Tests\Feature;

use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BlogTest extends TestCase
{
    use RefreshDatabase;

    public function test_blog_page_is_accessible(): void
    {
        $response = $this->get('/blog');

        $response->assertOk();
    }

    public function test_post_can_be_viewed(): void
    {
        $post = Post::factory()->create();

        $response = $this->get(
            '/blog/' . $post->slug
        );

        $response->assertOk();
    }
}

Chúng ta có thể tiếp tục mở rộng thành:

BlogTest
PostTest
CategoryTest
UserTest
ApiTest
AuthenticationTest

31. Test API Response Structure

Với API Resource, đây là một test rất đáng làm:

$response->assertJsonStructure([
    'data' => [
        '*' => [
            'id',
            'title',
            'slug',
        ],
    ],
]);

Nếu sau này developer vô tình xóa slug khỏi Resource:

Test FAIL

Nhờ vậy chúng ta phát hiện thay đổi API trước khi frontend hoặc mobile app gặp lỗi.


32. Test không chỉ để tìm Bug

Một cách hiểu tốt hơn:

Test là một lớp bảo vệ cho source code.

Ví dụ:

Hôm nay
    ↓
Code chạy đúng
    ↓
Viết Test
    ↓
3 tháng sau
    ↓
Sửa Controller
    ↓
Chạy Test
    ↓
PASS

Nếu:

FAIL

chúng ta biết thay đổi mới có thể đã ảnh hưởng chức năng cũ.


33. Regression Test

Đây gọi là Regression Testing.

Ví dụ:

Version 1
    ↓
Login hoạt động
    ↓
Version 2
    ↓
Thêm API
    ↓
Version 3
    ↓
Sửa User
    ↓
Login bị lỗi

Nếu có test:

php artisan test

chúng ta có thể phát hiện lỗi nhanh hơn.


34. Test Environment

Khi chạy test, Laravel sử dụng environment:

testing

Các thiết lập testing có thể được cấu hình trong phpunit.xml hoặc .env.testing. Laravel cũng cấu hình một số driver phù hợp cho testing, chẳng hạn session và cache mặc định không ghi dữ liệu vào hệ thống thật.

Có thể tạo:

.env.testing

Ví dụ:

APP_ENV=testing

DB_CONNECTION=sqlite
DB_DATABASE=:memory:

Tùy cấu hình project, bạn cũng có thể sử dụng một database testing riêng.

Điểm quan trọng:

Không chạy test trên database production.


35. phpunit.xml

Laravel project có:

phpunit.xml

File này chứa cấu hình dành cho PHPUnit.

Ví dụ có thể cấu hình:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="CACHE_STORE" value="array"/>
</php>

Không nên tùy tiện sửa cấu hình nếu chưa hiểu tác động của nó.


36. Chạy một Test cụ thể

Không cần chạy toàn bộ test suite.

Có thể chỉ chạy:

php artisan test --filter=BlogTest

Hoặc:

vendor/bin/phpunit --filter BlogTest

Điều này rất hữu ích khi đang phát triển một module.


37. Quy trình Testing cơ bản

Khi xây dựng một feature:

Requirement
     ↓
Code
     ↓
Test
     ↓
Run Test
     ↓
PASS?
  ┌──┴──┐
 YES    NO
  │      │
  ▼      ▼
Done   Debug
         │
         ▼
       Fix
         │
         └──────► Test lại

Đây là vòng lặp rất quan trọng trong phát triển phần mềm.


38. Testing Blog CMS

Với project cuối khóa, không cần test mọi dòng code.

Có thể ưu tiên các chức năng quan trọng:

Authentication
       ↓
Authorization
       ↓
Categories
       ↓
Posts
       ↓
Upload
       ↓
Soft Delete
       ↓
REST API
       ↓
Sanctum

Ví dụ:

tests/
│
├── Feature/
│   ├── AuthenticationTest.php
│   ├── CategoryTest.php
│   ├── PostTest.php
│   ├── UserTest.php
│   └── ApiTest.php
│
└── Unit/
    └── ExampleTest.php

Không cần biến project Blog CMS thành một hệ thống test khổng lồ.


39. Test quan trọng hơn Coverage 100%

Một người mới rất dễ nghĩ:

"Phải test 100% code."

Không nhất thiết.

100% coverage không đồng nghĩa:

100% không có bug

Điều quan trọng hơn là test những behavior quan trọng.

Ví dụ Blog CMS:

User không đăng nhập
→ không vào Admin

User thường
→ không quản lý User

Admin
→ quản lý User

Post draft
→ không xuất hiện public

Post published
→ xuất hiện Blog

API private
→ yêu cầu token

Upload sai file
→ validation fail

Đây là những behavior có giá trị cao để kiểm thử.


40. PHPUnit và CI/CD

Khi đã có test, sau này có thể đưa vào CI/CD:

Git Push
   │
   ▼
CI Server
   │
   ├── composer install
   ├── Run Tests
   ├── Build Assets
   └── Deploy

Nếu:

Tests PASS

→ có thể tiếp tục deployment.

Nếu:

Tests FAIL

→ dừng deployment.

Đây là bước phát triển tiếp theo sau khi đã nắm testing cơ bản.


41. PHPUnit Cheat Sheet

Một số lệnh và assertion cần nhớ:

Chạy toàn bộ test

php artisan test

Chạy một test

php artisan test --filter=BlogTest

HTTP 200

$response->assertOk();

HTTP status

$response->assertStatus(200);

Redirect

$response->assertRedirect('/login');

Có nội dung

$response->assertSee('Blog');

Có validation error

$response->assertSessionHasErrors([
    'title',
]);

JSON

$response->assertJson([
    'data' => [],
]);

JSON structure

$response->assertJsonStructure([
    'data',
]);

Unauthorized

$response->assertUnauthorized();

Forbidden

$response->assertForbidden();

42. Bài tập Bonus

Bài tập 1

Tạo:

tests/Feature/BlogTest.php

Test:

GET /blog

Yêu cầu:

HTTP 200

Bài tập 2

Test Post:

GET /blog/{slug}

Yêu cầu:

HTTP 200

với một Post hợp lệ.


Bài tập 3

Test validation khi tạo Post.

Thiếu:

title

Kết quả:

validation error

Bài tập 4

Test API:

GET /api/posts

Yêu cầu:

HTTP 200
JSON
data

Bài tập 5

Test API private:

GET /api/user

Không có token:

401

Có token:

200

Bài tập 6

Test Upload Image.

Kiểm tra:

image hợp lệ
→ upload thành công

file không hợp lệ
→ validation fail

43. Khi nào nên viết Test?

Không nhất thiết phải chờ project hoàn thành.

Có thể viết test ngay khi hoàn thành một feature:

Category
   ↓
Code
   ↓
Test

Post
   ↓
Code
   ↓
Test

API
   ↓
Code
   ↓
Test

Cách này giúp lỗi được phát hiện sớm.


44. Testing trong quy trình phát triển thực tế

Một workflow đơn giản:

1. Nhận requirement
        ↓
2. Thiết kế
        ↓
3. Code
        ↓
4. Viết Test
        ↓
5. Run Test
        ↓
6. Fix Bug
        ↓
7. Code Review
        ↓
8. Deploy

Ở project lớn hơn:

Developer
    ↓
Git
    ↓
CI
    ↓
Tests
    ↓
Build
    ↓
Deploy

45. Laravel 13 + PHPUnit — Tổng kết

Sau Bonus này, người học cần hiểu được:

PHPUnit
   │
   ├── Unit Test
   │
   ├── Feature Test
   │
   ├── HTTP Test
   │
   ├── Database Test
   │
   ├── API Test
   │
   ├── Authentication Test
   │
   ├── Validation Test
   │
   ├── Upload Test
   │
   └── Event / Queue / Mail Test

Và những assertion cơ bản:

assertOk()
assertStatus()
assertRedirect()
assertSee()
assertSessionHasErrors()
assertJson()
assertJsonStructure()
assertUnauthorized()
assertForbidden()

46. Hoàn thành Bonus PHPUnit

Toàn bộ phần Laravel của khóa học bây giờ có thể tóm tắt:

PHP
 │
 ▼
Laravel 13
 │
 ├── MVC
 ├── Routing
 ├── Controller
 ├── Blade
 │
 ├── Database
 ├── Eloquent
 ├── Relationship
 │
 ├── Authentication
 ├── Authorization
 │
 ├── CRUD
 ├── Upload
 ├── Search
 ├── Pagination
 ├── Soft Delete
 │
 ├── Event
 ├── Queue
 ├── Mail
 ├── Notification
 ├── Cache
 ├── Storage
 ├── Logging
 │
 ├── REST API
 ├── Resource
 ├── Sanctum
 ├── API Authentication
 │
 ├── VPS
 ├── Nginx
 ├── Queue Worker
 ├── Scheduler
 └── Production

          +

       PHPUnit
          │
          ▼
       Testing

47. Điều quan trọng nhất

Testing không phải là việc viết thật nhiều code test.

Điều quan trọng là xác định:

Ứng dụng phải làm gì?

Sau đó biến yêu cầu đó thành test.

Ví dụ:

Requirement:

"User chưa đăng nhập không được vào Admin."

             ↓

Test:

GET /admin/users

             ↓

Expected:

401 / Redirect Login

Hoặc:

Requirement:

"API Posts phải trả JSON."

             ↓

Test:

GET /api/posts

             ↓

Expected:

200
JSON
data

Hoặc:

Requirement:

"User thường không được quản lý User."

             ↓

Test:

User → GET /admin/users

             ↓

Expected:

403

Đó chính là tư duy testing.


48. Kết luận

Sau 50 bài Laravel 13 và Bonus PHPUnit, chúng ta đã đi qua toàn bộ quy trình cơ bản của một ứng dụng Web:

PHP
 ↓
Laravel
 ↓
Database
 ↓
Eloquent
 ↓
CRUD
 ↓
Authentication
 ↓
Authorization
 ↓
API
 ↓
Sanctum
 ↓
Queue
 ↓
Cache
 ↓
Storage
 ↓
Logging
 ↓
Testing
 ↓
Deployment

Người học không cần nhớ mọi method của Laravel.

Điều quan trọng hơn là biết:

Tìm tài liệu
     ↓
Hiểu vấn đề
     ↓
Thiết kế
     ↓
Viết code
     ↓
Test
     ↓
Debug
     ↓
Deploy

Laravel là framework.

PHP là ngôn ngữ.

Database là nền tảng dữ liệu.

Testing là lớp bảo vệ.

Deployment đưa sản phẩm đến người dùng.

Và Blog CMS chính là project kết nối tất cả những kiến thức đó lại.


🎓 HOÀN THÀNH PHẦN LARAVEL 13

┌─────────────────────────────────────────┐
│                                         │
│          PHP → LARAVEL 13               │
│                                         │
│       50 BÀI + BONUS PHPUnit            │
│                                         │
│        BLOG CMS PROJECT                 │
│                                         │
│  CRUD • AUTH • API • SANCTUM            │
│  QUEUE • CACHE • STORAGE • LOGGING      │
│  TESTING • NGINX • VPS • PRODUCTION     │
│                                         │
└─────────────────────────────────────────┘

Hoàn thành phần Laravel 13.

Bước tiếp theo không còn là học thêm syntax một cách máy móc.

Hãy lấy Blog CMS làm nền tảng, sau đó tự xây dựng một project mới:

Ý tưởng
   ↓
Database
   ↓
Laravel
   ↓
CRUD
   ↓
Authentication
   ↓
API
   ↓
Testing
   ↓
Deploy

Đó mới là lúc kiến thức Laravel bắt đầu trở thành kỹ năng lập trình thực tế.

Không có nhận xét nào:

Đăng nhận xét

Facebook Youtube RSS