- การทำ Unit Test ใน Laravel 13 ด้วย PHPUnit แบบทีละขั้นตอน
- 1. Unit Test คืออะไร
- 2. Unit Test กับ Feature Test ต่างกันอย่างไร
- 3. สร้าง Laravel 13 Project
- 4. โครงสร้าง Tests ของ Laravel
- 5. Workshop: สร้าง PriceCalculator
- 6. สร้าง Unit Test
- 7. รัน Unit Test
- 8. เพิ่ม Test Case หลายกรณี
- 9. ทดสอบ Exception
- 10. Assertions ที่ควรรู้
- 11. ตัวอย่างการทดสอบ Business Logic: GradeCalculator
- 12. ใช้ Data Provider ลด Test Code ซ้ำ
- 13. Unit Test ที่มี Dependency
- 14. Mock Dependency ด้วย PHPUnit
- 15. Unit Test ไม่ควรใช้ Database หรือไม่
- 16. การตั้งชื่อ Test ที่ดี
- 17. แนวคิด FIRST สำหรับ Unit Test
- 18. Unit Test ใน Development Workflow
- 19. คำสั่งที่ใช้บ่อย
- 20. Checklist ก่อนเขียน Unit Test
- 21. สรุป
- แหล่งข้อมูลเพิ่มเติม
#การทำ Unit Test ใน Laravel 13 ด้วย PHPUnit แบบทีละขั้นตอน
การทดสอบซอฟต์แวร์เป็นส่วนสำคัญของการพัฒนา Application ที่ต้องการความถูกต้องและดูแลรักษาได้ง่าย โดยเฉพาะระบบที่มี Business Logic จำนวนมาก การเขียน Unit Test ช่วยให้เราตรวจสอบแต่ละหน่วยของโปรแกรมได้โดยอัตโนมัติ และช่วยลดโอกาสเกิด Regression เมื่อมีการแก้ไขโค้ดในอนาคต
บทความนี้สาธิตการทำ Unit Test ใน Laravel 13 โดยใช้ PHPUnit ตั้งแต่การเตรียมโปรเจกต์ การสร้าง Service การสร้าง Test Case การใช้ Assertions การทดสอบ Exception ไปจนถึงการ Mock Dependency
แนวคิดสำคัญ: Unit Test ควรทดสอบหน่วยของโค้ดขนาดเล็กแบบแยกส่วน (isolated) และไม่ควรพึ่ง Database, HTTP Request หรือ External Service โดยไม่จำเป็น
#1. Unit Test คืออะไร
Unit Test คือการทดสอบหน่วยย่อยของโปรแกรม เช่น Method, Function หรือ Class เพื่อยืนยันว่า logic ของหน่วยนั้นให้ผลลัพธ์ตรงตามที่ออกแบบไว้
ตัวอย่างเช่น เรามีระบบคำนวณส่วนลด:
ราคา 1,000 บาท
ส่วนลด 10%
ผลลัพธ์ที่คาดหวัง = 900 บาท
แทนที่จะเปิด Browser และกรอกข้อมูลด้วยตนเอง เราสามารถสร้าง Test Case เพื่อเรียก Method คำนวณราคาโดยตรงและตรวจสอบผลลัพธ์อัตโนมัติ
Input
↓
Method / Class
↓
Actual Result
↓
Assertion
↓
Expected Result
↓
PASS / FAIL
#2. Unit Test กับ Feature Test ต่างกันอย่างไร
Laravel แบ่งการทดสอบโดยทั่วไปออกเป็นสองกลุ่มสำคัญ
| Unit Test | Feature Test |
|---|---|
| ทดสอบ Class/Method ขนาดเล็ก | ทดสอบหลายส่วนทำงานร่วมกัน |
| เน้น isolated logic | ใช้งาน Laravel framework ได้เต็มกว่า |
| ไม่ควรเชื่อม Database โดยไม่จำเป็น | สามารถทดสอบ Database ได้ |
| ไม่จำเป็นต้องส่ง HTTP Request | เหมาะกับ HTTP/API testing |
| ทำงานเร็ว | มักใช้เวลามากกว่า Unit Test |
อยู่ใน tests/Unit |
อยู่ใน tests/Feature |
ตัวอย่างงานที่เหมาะกับ Unit Test ได้แก่ การคำนวณราคา ภาษี คะแนน เกรด การแปลงข้อมูล หรือ validation logic ที่เขียนเป็น class แยก
ส่วนการทดสอบ endpoint เช่น
POST /api/users
รวมถึง Authentication, Middleware, Validation และ Database เหมาะกับ Feature Test มากกว่า
#3. สร้าง Laravel 13 Project
ตรวจสอบ PHP ก่อน:
php -v
จากนั้นสร้างโปรเจกต์ เช่น
composer create-project laravel/laravel laravel-unit-test
cd laravel-unit-test
ตรวจสอบ Laravel:
php artisan --version
และทดลองรัน test ที่มากับโปรเจกต์:
php artisan test
#4. โครงสร้าง Tests ของ Laravel
โครงสร้างหลักจะมีลักษณะดังนี้
laravel-unit-test/
├── app/
│ ├── Http/
│ ├── Models/
│ └── Services/
│
├── tests/
│ ├── Feature/
│ │ └── ExampleTest.php
│ └── Unit/
│ └── ExampleTest.php
│
├── phpunit.xml
└── artisan
ในบทความนี้เราจะเน้นไฟล์ใน
tests/Unit/
#5. Workshop: สร้าง PriceCalculator
สร้าง directory:
app/Services
จากนั้นสร้างไฟล์
app/Services/PriceCalculator.php
<?php
namespace App\Services;
use InvalidArgumentException;
class PriceCalculator
{
public function calculateDiscount(float $price, float $percent): float
{
if ($price < 0) {
throw new InvalidArgumentException('Price cannot be negative');
}
if ($percent < 0 || $percent > 100) {
throw new InvalidArgumentException('Discount must be between 0 and 100');
}
return $price - ($price * $percent / 100);
}
}
Class นี้รับราคาและเปอร์เซ็นต์ส่วนลด แล้วคืนราคาสุทธิ
final price = price - (price × percent / 100)
ตัวอย่าง
price = 1000
percent = 10
1000 - (1000 × 10 / 100)
= 900
#6. สร้าง Unit Test
ใช้ Artisan:
php artisan make:test PriceCalculatorTest --unit
Laravel จะสร้างไฟล์
tests/Unit/PriceCalculatorTest.php
แก้ไขเป็น
<?php
namespace Tests\Unit;
use App\Services\PriceCalculator;
use PHPUnit\Framework\TestCase;
class PriceCalculatorTest extends TestCase
{
public function test_calculate_discount(): void
{
$calculator = new PriceCalculator();
$result = $calculator->calculateDiscount(1000, 10);
$this->assertEquals(900, $result);
}
}
Test นี้แบ่งแนวคิดได้เป็น Arrange → Act → Assert (AAA)
// Arrange
$calculator = new PriceCalculator();
// Act
$result = $calculator->calculateDiscount(1000, 10);
// Assert
$this->assertEquals(900, $result);
#7. รัน Unit Test
รัน test ทั้งหมด:
php artisan test
รันเฉพาะ Unit suite:
php artisan test --testsuite=Unit
รันเฉพาะไฟล์:
php artisan test tests/Unit/PriceCalculatorTest.php
หรือกรองชื่อ test:
php artisan test --filter=PriceCalculatorTest
เมื่อผลลัพธ์ถูกต้อง test จะแสดงสถานะผ่าน หาก expected value ไม่ตรงกับ actual value test จะล้มเหลวและแสดงรายละเอียดเพื่อช่วยวิเคราะห์ปัญหา
#8. เพิ่ม Test Case หลายกรณี
Unit Test ที่ดีไม่ควรตรวจสอบเฉพาะ Happy Path
public function test_zero_discount_returns_original_price(): void
{
$calculator = new PriceCalculator();
$result = $calculator->calculateDiscount(1000, 0);
$this->assertEquals(1000, $result);
}
ทดสอบส่วนลด 100%:
public function test_full_discount_returns_zero(): void
{
$calculator = new PriceCalculator();
$result = $calculator->calculateDiscount(1000, 100);
$this->assertEquals(0, $result);
}
จึงควรคิดอย่างน้อยถึง
Normal Case
Boundary Case
Invalid Input
Exception Case
#9. ทดสอบ Exception
กรณีส่วนลดเกิน 100%:
public function test_discount_greater_than_100_throws_exception(): void
{
$calculator = new PriceCalculator();
$this->expectException(\InvalidArgumentException::class);
$calculator->calculateDiscount(1000, 120);
}
สามารถตรวจสอบข้อความ Exception เพิ่มได้:
public function test_invalid_discount_has_correct_message(): void
{
$calculator = new PriceCalculator();
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Discount must be between 0 and 100');
$calculator->calculateDiscount(1000, 120);
}
และกรณีราคาติดลบ:
public function test_negative_price_throws_exception(): void
{
$calculator = new PriceCalculator();
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Price cannot be negative');
$calculator->calculateDiscount(-100, 10);
}
#10. Assertions ที่ควรรู้
PHPUnit มี Assertions จำนวนมาก ตัวอย่างที่ใช้บ่อย ได้แก่
$this->assertEquals($expected, $actual);
$this->assertSame($expected, $actual);
$this->assertTrue($value);
$this->assertFalse($value);
$this->assertNull($value);
$this->assertNotNull($value);
$this->assertCount(3, $items);
$this->assertEmpty($items);
$this->assertContains('Laravel', $items);
$this->assertInstanceOf(User::class, $user);
assertEquals() เน้นความเท่ากันของค่า ขณะที่ assertSame() ตรวจสอบทั้งค่าและชนิดข้อมูลอย่างเข้มงวดกว่า จึงควรเลือกให้เหมาะกับสิ่งที่ต้องการยืนยัน
#11. ตัวอย่างการทดสอบ Business Logic: GradeCalculator
สร้าง
app/Services/GradeCalculator.php
<?php
namespace App\Services;
use InvalidArgumentException;
class GradeCalculator
{
public function calculate(float $score): string
{
if ($score < 0 || $score > 100) {
throw new InvalidArgumentException('Score must be between 0 and 100');
}
return match (true) {
$score >= 80 => 'A',
$score >= 70 => 'B',
$score >= 60 => 'C',
$score >= 50 => 'D',
default => 'F',
};
}
}
สร้าง test:
php artisan make:test GradeCalculatorTest --unit
<?php
namespace Tests\Unit;
use App\Services\GradeCalculator;
use PHPUnit\Framework\TestCase;
class GradeCalculatorTest extends TestCase
{
public function test_score_80_returns_grade_a(): void
{
$calculator = new GradeCalculator();
$this->assertSame('A', $calculator->calculate(80));
}
public function test_score_79_returns_grade_b(): void
{
$calculator = new GradeCalculator();
$this->assertSame('B', $calculator->calculate(79));
}
public function test_score_49_returns_grade_f(): void
{
$calculator = new GradeCalculator();
$this->assertSame('F', $calculator->calculate(49));
}
}
Boundary values เช่น 79/80, 69/70 และ 49/50 มีความสำคัญ เพราะข้อผิดพลาดในเงื่อนไขมักเกิดบริเวณขอบเขต
#12. ใช้ Data Provider ลด Test Code ซ้ำ
แทนที่จะเขียน method จำนวนมาก สามารถใช้ Data Provider ของ PHPUnit ได้
<?php
namespace Tests\Unit;
use App\Services\GradeCalculator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class GradeCalculatorTest extends TestCase
{
#[DataProvider('gradeProvider')]
public function test_calculate_grade(float $score, string $expected): void
{
$calculator = new GradeCalculator();
$this->assertSame($expected, $calculator->calculate($score));
}
public static function gradeProvider(): array
{
return [
'A lower boundary' => [80, 'A'],
'A maximum' => [100, 'A'],
'B lower boundary' => [70, 'B'],
'B upper boundary' => [79, 'B'],
'C lower boundary' => [60, 'C'],
'D lower boundary' => [50, 'D'],
'F upper boundary' => [49, 'F'],
'F minimum' => [0, 'F'],
];
}
}
วิธีนี้เหมาะกับ logic ที่ต้องทดสอบ Input/Expected Output หลายชุด
#13. Unit Test ที่มี Dependency
ในระบบจริง Service หนึ่งอาจเรียก Service อื่น เช่นระบบส่ง Notification
OrderService
↓
NotificationService
หากต้องการ Unit Test OrderService เราไม่ควรส่ง Notification จริง แต่ควรแทน dependency ด้วย Mock
ตัวอย่าง Interface:
<?php
namespace App\Contracts;
interface NotificationService
{
public function send(string $message): bool;
}
สร้าง OrderService:
<?php
namespace App\Services;
use App\Contracts\NotificationService;
class OrderService
{
public function __construct(
private NotificationService $notification
) {}
public function complete(): bool
{
$this->notification->send('Order completed');
return true;
}
}
#14. Mock Dependency ด้วย PHPUnit
<?php
namespace Tests\Unit;
use App\Contracts\NotificationService;
use App\Services\OrderService;
use PHPUnit\Framework\TestCase;
class OrderServiceTest extends TestCase
{
public function test_complete_sends_notification(): void
{
$notification = $this->createMock(NotificationService::class);
$notification
->expects($this->once())
->method('send')
->with('Order completed')
->willReturn(true);
$service = new OrderService($notification);
$result = $service->complete();
$this->assertTrue($result);
}
}
Mock ช่วยให้ test ไม่ต้องเชื่อมต่อ Email, LINE, Payment Gateway, REST API หรือบริการภายนอกจริง ทำให้ test เร็วและ deterministic มากขึ้น
#15. Unit Test ไม่ควรใช้ Database หรือไม่
สำหรับ pure Unit Test ควรหลีกเลี่ยง Database เพราะเป้าหมายคือทดสอบ logic แยกส่วน
ตัวอย่างที่ไม่เหมาะกับ Unit Test:
User::create([
'name' => 'John',
'email' => 'john@example.com',
]);
เมื่อ test ต้องพึ่ง Eloquent และ Database มักเหมาะกับ Feature Test เช่น
php artisan make:test UserRegistrationTest
จากนั้นสามารถใช้เครื่องมืออย่าง RefreshDatabase และ Laravel testing helpers ได้ตามลักษณะการทดสอบ
#16. การตั้งชื่อ Test ที่ดี
ชื่อ Test ควรบอก behavior ที่ต้องการตรวจสอบ
ไม่แนะนำ:
public function test1(): void
ดีกว่า:
public function test_calculate_discount_returns_correct_price(): void
หรือ
public function test_negative_price_throws_exception(): void
เมื่อ test fail เราจะเข้าใจปัญหาได้ทันทีจากชื่อ test
#17. แนวคิด FIRST สำหรับ Unit Test
Unit Test ที่ดีสามารถพิจารณาหลัก FIRST ได้
| หลักการ | ความหมาย |
|---|---|
| Fast | Test ควรรันเร็ว |
| Independent | Test ไม่ควรขึ้นต่อกัน |
| Repeatable | รันซ้ำแล้วควรได้ผลที่สม่ำเสมอ |
| Self-validating | Test ตัดสิน PASS/FAIL ได้เอง |
| Timely | ควรเขียน test ใกล้กับช่วงที่พัฒนา feature |
ดังนั้นไม่ควรออกแบบ test เช่น
Test A → สร้างข้อมูล
↓
Test B → ต้องใช้ข้อมูลจาก Test A
Test B ควรเตรียมข้อมูลของตัวเองเพื่อไม่ให้ลำดับการรันมีผลต่อผลทดสอบ
#18. Unit Test ใน Development Workflow
Unit Test สามารถนำเข้า workflow ได้ดังนี้
Developer
↓
Write Code
↓
Write Unit Test
↓
php artisan test
↓
PASS ?
├── No → Fix Code → Test Again
└── Yes
↓
Git Commit
↓
Git Push
↓
GitHub Actions / CI
↓
Automated Test
↓
Build / Deploy
เมื่อใช้ร่วมกับ CI/CD ทุกครั้งที่ Push หรือสร้าง Pull Request ระบบสามารถรัน test อัตโนมัติก่อน merge หรือ deploy ได้
#19. คำสั่งที่ใช้บ่อย
# สร้าง Unit Test
php artisan make:test PriceCalculatorTest --unit
# รัน Test ทั้งหมด
php artisan test
# รันเฉพาะ Unit Test
php artisan test --testsuite=Unit
# รันเฉพาะไฟล์
php artisan test tests/Unit/PriceCalculatorTest.php
# รันตามชื่อ
php artisan test --filter=PriceCalculatorTest
# หยุดเมื่อพบ failure แรก
php artisan test --stop-on-failure
สามารถเรียก PHPUnit โดยตรงได้เช่นกัน แต่ในโปรเจกต์ Laravel การใช้ php artisan test มักสะดวกและสอดคล้องกับ workflow ของ framework
#20. Checklist ก่อนเขียน Unit Test
ก่อนสร้าง Test Case ลองตอบคำถามต่อไปนี้
- หน่วยของโค้ดที่กำลังทดสอบคืออะไร
- Input คืออะไร
- Expected Output คืออะไร
- มี Boundary Value ใดบ้าง
- Invalid Input มีอะไรบ้าง
- ต้องเกิด Exception เมื่อใด
- Class มี External Dependency หรือไม่
- Dependency นั้นควร Mock หรือไม่
- Test สามารถรันแยกจาก test อื่นได้หรือไม่
- Test พึ่ง Database/HTTP มากเกินไปจนควรเป็น Feature Test หรือไม่
#21. สรุป
Laravel 13 มีโครงสร้างรองรับ Automated Testing ตั้งแต่เริ่มต้น และสามารถใช้ PHPUnit เพื่อสร้าง Unit Test สำหรับ business logic ได้อย่างเป็นระบบ
แนวทางสำคัญคือ
Business Logic
↓
Unit Test
↓
Arrange → Act → Assert
↓
Normal + Boundary + Invalid Cases
↓
Mock External Dependencies
↓
php artisan test
↓
CI/CD
สำหรับผู้เริ่มต้น ควรเริ่มจาก Class ที่ไม่มี dependency เช่น PriceCalculator หรือ GradeCalculator ก่อน แล้วจึงเพิ่ม Exception Testing, Data Provider และ Mocking ตามลำดับ เมื่อ test เริ่มเกี่ยวข้องกับ HTTP, Database, Authentication หรือหลาย component ของ Laravel ควรพิจารณาย้ายไปใช้ Feature Test เพื่อให้ตรงกับระดับของการทดสอบ
#แหล่งข้อมูลเพิ่มเติม
- Laravel Documentation — Testing: https://laravel.com/docs/13.x/testing
- PHPUnit Documentation: https://docs.phpunit.de/