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.php9. 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:
- Xóa config cache
- Tạo file
.env.testingvà 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=testingNộ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:listVí 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 class28. 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 class30. 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




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