NTM Solutions

Bài đăng nổi bật

🚀 Laravel 12 (2026) — Từ PHP Core đến Full Stack📘

Nếu cập nhật theo Laravel 12 LTS (2026) thì mình sẽ chia thành các phần như sau để người học đi từ PHP Core → Laravel → Triển khai dự án th...

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.

p/S: mục 8 trở đi mới chạy được.


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
// php artisan make:test BlogTest
namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

//php artisan test --filter=BlogTest
class BlogTest extends TestCase
{
    use RefreshDatabase;

    // Tự động nạp dữ liệu mẫu từ DatabaseSeeder vào database testing
    protected $seed = true;
    
    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

Trước khi chạy Test phải vào file Pest.php comment dòng này lại nếu không sẽ kích hoạt refresh database mất hết dữ liệu.

pest()->extend(TestCase::class)
  // ->use(RefreshDatabase::class) // Comment dòng này lại
  ->in('Feature');

Tóm lại: 

dùng file .env.testing ngay từ đầu + thêm biến seed để laravel lấy dữ liệu mẫu tránh chung đụng dữ liệu testing và dữ liệu thật.

Xem mục 17. Test Post

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.


Ghi chú:

1.lệnh chạy trực tiếp phpunit ngoài thư mục gốc phải gõ dấu \ mới chạy

vendor\bin\phpunit tests\Feature\BlogTest.php

2.nên chạy riêng BlogTest.php để tránh hiển thị quá nhiều

php artisan test tests/Feature/BlogTest.php

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.


Ghi chú: ví dụ trên chỉ đúng nếu trả về api/posts/1

Đối với trường hợp trả về nguyên mảng phải viết lại:

// test API danh sách posts
public function test_posts_api_returns_success(): void
{
    $response = $this->getJson('/api/posts');

    $response->assertStatus(200)
        ->assertJsonStructure([
            'success',
            'data' => [
                '*' => [ // <--- Dấu * đại diện cho từng phần tử trong mảng data
                    'id',
                    'title',
                    'slug',
                    'excerpt',
                    'image_url',
                    'published_at',
                    'is_published',
                    'views',
                    'author' => ['id', 'name', 'email', 'role', 'created_at'],
                    'category' => ['id', 'name', 'slug']
                ]
            ]
        ]);
}

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

Trước khi chạy ví dụ dưới để tránh mất data phải:

  1. Xóa config cache
  2. Tạo file .env.testing và kiểm tra xem laravel đang nhận database nào lúc chạy test

php artisan config:clear
php artisan env --env=testing
php artisan config:show database.default --env=testing

Nội dung file .env.testing

APP_ENV=testing
APP_KEY=base64:key

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_db_test
DB_USERNAME=root
DB_PASSWORD=

# Bắt buộc xóa hẳn hoặc để trống dòng DB_URL
DB_URL=

Nhớ vào phpmyadmin tạo database có tên là: your_db_test

database này sẽ trống trơn do phpunit.xml cấu hình chạy trên RAM.

Data lúc này dựa vào Factory() hoặc $seed (tạo toàn bộ db -> khá nặng) như trường hợp BlogTest

Phải sửa trong PostFactory

// Khai báo mối quan hệ theo chuẩn Laravel
'user_id' => User::factory(),
'category_id' => Category::factory(),

Ví dụ:

<?php
namespace Tests\Feature;

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

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_post_page_can_be_opened(): void
    {
        $user = User::factory()->create();
        $category = Category::factory()->create();

        // Ép status = published để route tìm thấy bài viết
        $post = Post::factory()->create([
            'user_id' => $user->id,
            'category_id' => $category->id,
            'status' => 'published',
            'published_at' => now(),
        ]);

        $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 (đôi khi user thường k cho vào phải role admin):

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

$this->actingAs($user);

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

// Đã đăng nhập -> Phải trả về 200 OK thành công
$response->assertOk();

Đâ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.

Full code:

/**
 * Test validation khi tạo bài viết mới thiếu các trường bắt buộc.
 */

public function test_create_post_validation_fails_when_required_fields_are_missing(): void
{
    $admin = User::factory()->create(['role' => 'admin']);
    $this->actingAs($admin);

    // Gửi request rỗng
    $response = $this->post('/admin/posts', []);

    // Kiểm tra đúng 4 trường bắt buộc (có chữ required)
    $response->assertSessionHasErrors([
        'title',
        'slug',
        'content',
        'status',
    ]);

    $response->assertStatus(302);
}

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.

Trước tiên vào app/Events/ xem có event nào hoặc dùng lệnh:

php artisan event:list

Ví dụ: PostPublished và UserRegistered (đã học trong bài Event)

Sau đó vào PostController xem hàm store dòng:

event(new PostPublished($post));

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

Dưới đây là full code passed

use App\Events\PostPublished;
use App\Events\UserRegistered;
use App\Models\Post;
use App\Models\User;
use App\Models\Category; 
use Illuminate\Support\Facades\Event;

function test_user_registered_event_is_dispatched(): void
{
    Event::fake();

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

    event(new UserRegistered($user));

    Event::assertDispatched(
    	UserRegistered::class
    );
}

//test event published_post bắn ra khi store post thành công chọn published
public function test_creating_published_post_dispatches_event(): void
{
    Event::fake();
    
    $admin = User::factory()->create([
        'role' => 'admin',
    ]);
    
    $category = Category::factory()->create();
    
    $this->actingAs($admin);
    
    $response = $this->post('/admin/posts', [
        'category_id' => $category->id,
        'title' => 'Laravel Testing',
        'slug' => 'laravel-testing',
        'content' => str_repeat('Nội dung test ', 5),
        'status' => 'published',
    ]);
    
    Event::assertDispatched(
        PostPublished::class,
        fn (PostPublished $event)
        => $event->post->title === 'Laravel Testing'
    );
}

//test event published_post k bắn ra khi draft
public function test_creating_draft_post_does_not_dispatch_event(): void
{
    Event::fake();
    
    $admin = User::factory()->create([
        'role' => 'admin',
    ]);
    
    $category = Category::factory()->create();
    
    $this->actingAs($admin);
    
    $this->post('/admin/posts', [
        'category_id' => $category->id,
        'title' => 'Draft Post',
        'slug' => 'draft-post',
        'content' => str_repeat('Nội dung test ', 5),
        'status' => 'draft',
    ]);
    
    Event::assertNotDispatched(
        PostPublished::class
    );
}

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.

use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Support\Facades\Queue;

public function test_welcome_email_job_is_queued(): void
{
    Queue::fake();
    
    $user = User::factory()->create();
    
    SendWelcomeEmail::dispatch($user);
    
    Queue::assertPushed(
        SendWelcomeEmail::class
    );
}

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

Full Code MailTest:

//php artisan make:test MailTest
namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeUser;

//php artisan test tests/Feature/MailTest.php

class MailTest extends TestCase
{
    public function test_user_receives_welcome_email()
    {
        Mail::fake();
        
        Mail::to('test@example.com')
        ->send(new WelcomeUser('Nguyễn Văn A'));
        
        Mail::assertSent(WelcomeUser::class, function ($mail) {
            return $mail->hasTo('test@example.com')
            && $mail->name === 'Nguyễn Văn A';
        });
    }
}
//END class

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.

Full code UploadTest:

// php artisan make:test UploadTest
namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
use App\Models\User;
use App\Models\Country;

// php artisan test tests/Feature/UploadTest.php

class UploadTest extends TestCase
{
    use RefreshDatabase;

    public function test_image_can_be_uploaded()
    {
        Storage::fake('public');

        $user = User::factory()->create([
            'role' => 'admin',
        ]);
        
        $file = UploadedFile::fake()
        ->image('post.jpg');
        
        $response = $this->actingAs($user)
            ->post('/admin/posts', [
            'title' => 'Laravel 13',
            'slug' => 'laravel-13',
            'content' => str_repeat('Nội dung test ', 5),
            'status' => 'draft',
            'image' => $file,
        ]);
        
        $response->assertRedirect();
        
        Storage::disk('public')->assertExists(
            'posts/' . $file->hashName()
        );
    }
}
//END class

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();

Full code SoftDeleteTest:

// php artisan make:test SoftDeleteTest
namespace Tests\Feature;

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

// php artisan test tests/Feature/SoftDeleteTest.php
class SoftDeleteTest extends TestCase
{
    use RefreshDatabase;

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

        $post->delete();

        $this->assertSoftDeleted($post);
    }

    public function test_deleted_at_is_not_null_after_delete()
    {
        $post = Post::factory()->create();
        
        $post->delete();
        
        $this->assertNotNull(
        $post->fresh()->deleted_at
        );
    }

    public function test_post_can_be_restored()
    {
        $post = Post::factory()->create();
        
        $post->delete();
        
        $this->assertSoftDeleted($post);
        
        $post->restore();
        
        $this->assertNull(
            $post->fresh()->deleted_at
        );
    }
}

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.

<?php
// php artisan make:test CRUDPostTest
namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

// php artisan test --filter=CRUDPostTest
class CRUDPostTest extends TestCase
{
    use RefreshDatabase;

    protected User $user;

    protected function setUp(): void
    {
        parent::setUp();

        // Tạo Admin User và authenticate toàn bộ request trong class
        $this->user = User::factory()->create([
            'role' => 'admin',
        ]);

        $this->actingAs($this->user);
    }

    /**
     * Test 1: Hiển thị danh sách bài viết (Index)
     */
    public function test_can_list_posts(): void
    {
        Post::factory()->count(5)->create([
            'status' => 'published',
        ]);

        $response = $this->get(route('admin.posts.index'));

        $response->assertStatus(200);
    }

    /**
     * Test 2: Xem chi tiết bài viết (Show)
     */
    public function test_can_show_post(): void
    {
        $post = Post::factory()->create([
            'title' => 'Bài viết thử nghiệm',
            'slug' => 'bai-viet-thu-nghiem',
            'content' => 'Nội dung chi tiết bài viết',
            'status' => 'published',
        ]);

        $response = $this->get(route('admin.posts.show', $post));

        $response->assertStatus(200);
    }

    /**
     * Test 3: Tạo mới bài viết (Store)
     */
    public function test_can_create_post(): void
    {
        // Đảm bảo gửi ĐỦ dữ liệu theo đúng validation của validatePost()
        $payload = [
            'title'   => 'Tựa đề bài viết mới',
            'slug'    => 'tua-de-bai-viet-moi-test',
            'content' => 'Nội dung bài viết tạo từ test case',
            'status'  => 'draft',
            'user_id' => $this->user->id,
            'image'   => UploadedFile::fake()->image('post.jpg'), // Giả lập file ảnh thật
        ];

        $response = $this->post(route('admin.posts.store'), $payload);

        // Bắt buộc check không dính lỗi Validation
        //$response->assertSessionHasNoErrors();
        $response->assertSessionHasNoErrors()
            ->assertRedirect(route('admin.posts.show', 1));

        $this->assertDatabaseHas('posts', [
            'title' => 'Tựa đề bài viết mới',
            'slug' => 'tua-de-bai-viet-moi-test',
            'status' => 'draft',
        ]);
    }

    /**
     * Test 4: Cập nhật bài viết (Update)
     */
    public function test_can_update_post(): void
    {
        // Tạo post ban đầu có user_id thuộc về $this->user
        $post = Post::factory()->create([
            'user_id' => $this->user->id,
            'title' => 'Tựa đề cũ',
            'slug' => 'tua-de-cu-test',
            'content' => 'Nội dung cũ',
            'status' => 'draft',
        ]);

        $payload = [
            'title' => 'Tựa đề đã cập nhật',
            'slug' => 'tua-de-cu-test', // Giữ nguyên slug cũ để pass rule unique:posts,slug,{id}
            'content' => 'Nội dung đã được chỉnh sửa mới',
            'status' => 'published',
            'user_id' => $this->user->id,
        ];

        $response = $this->put(route('admin.posts.update', $post), $payload);

        //$response->assertSessionHasNoErrors();
        $response->assertSessionHasNoErrors()
            ->assertRedirect(route('admin.posts.show', 1));

        $this->assertDatabaseHas('posts', [
            'id' => $post->id,
            'title' => 'Tựa đề đã cập nhật',
            'status' => 'published',
        ]);
    }

    /**
     * Test 5: Xóa mềm bài viết (Destroy)
     */
    public function test_can_delete_post(): void
    {
        $post = Post::factory()->create([
            'user_id' => $this->user->id,
            'status' => 'published',
        ]);

        $response = $this->delete(route('admin.posts.destroy', $post));

        //$response->assertSessionHasNoErrors();
        $response->assertSessionHasNoErrors()
            ->assertRedirect(route('admin.posts.index'));

        $this->assertSoftDeleted('posts', [
            'id' => $post->id,
        ]);
    }

    /**
     * Test 6: Khôi phục bài viết đã xóa mềm (Restore - Route PATCH)
     */
    public function test_can_restore_post(): void
    {
        $post = Post::factory()->create([
            'status' => 'published',
        ]);

        $post->delete();

        $this->assertSoftDeleted('posts', [
            'id' => $post->id,
        ]);

        // Dùng URL trực tiếp /admin/posts/{id}/restore với PATCH để tránh bị route model binding chặn record đã soft delete
        $response = $this->patch(route('admin.posts.restore', $post->id));

        $this->assertNotSoftDeleted('posts', [
            'id' => $post->id,
        ]);
    }
}
//END class

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ó.

Ghi chú: chỉnh sửa trong đây phải clear config.


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()

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.

x1

quay về MỤC LỤC

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

Đăng nhận xét

Facebook Youtube RSS