- 1. เครื่องมือที่ใช้
- 2. Composer คืออะไร
- 3. สร้างโปรเจกต์
- 4. กำหนด PSR-4 Autoloading
- ส่วนที่ 1: PHPUnit
- ส่วนที่ 2: Pest
- ส่วนที่ 3: Data-driven Testing
- ส่วนที่ 4: ทดสอบ Business Logic
- ส่วนที่ 5: Composer Scripts
- ส่วนที่ 6: PHPUnit Configuration
- ส่วนที่ 7: PHPUnit vs Pest
- ส่วนที่ 8: Unit Test ที่ดีควรเป็นอย่างไร
- ส่วนที่ 9: Workflow ที่แนะนำ
- ส่วนที่ 10: Composer ใน CI/CD
- สรุป
การทดสอบระดับหน่วยหรือ Unit Testing เป็นกระบวนการตรวจสอบหน่วยย่อยของโปรแกรม เช่น method, function หรือ class ให้ทำงานตรงตามที่ออกแบบไว้ โดยแยกการทดสอบออกจากระบบภายนอกให้มากที่สุด
สำหรับระบบที่พัฒนาด้วย PHP เครื่องมือที่นิยมใช้ ได้แก่ PHPUnit และ Pest ขณะที่ Composer ทำหน้าที่จัดการ dependencies, autoloading และคำสั่งที่เกี่ยวข้องกับการทดสอบ
บทความนี้เป็น Workshop ตั้งแต่สร้าง PHP Project จนสามารถรัน Unit Test ด้วย PHPUnit และ Pest ผ่าน Composer ได้
#1. เครื่องมือที่ใช้
ตัวอย่างนี้ใช้เครื่องมือหลักดังนี้
- PHP
- Composer
- PHPUnit
- Pest
- PSR-4 Autoloading
ตรวจสอบ PHP:
php --version
ตรวจสอบ Composer:
composer --version
#2. Composer คืออะไร
Composer คือ Dependency Manager สำหรับ PHP ใช้ติดตั้งและบริหาร package ที่โปรเจกต์ต้องการ เช่น
- PHPUnit
- Pest
- Laravel packages
- Symfony components
- Logging libraries
- HTTP clients
ไฟล์สำคัญคือ:
composer.json
composer.lock
composer.json ใช้ประกาศ dependencies และ configuration ของโปรเจกต์ ส่วน
composer.lock บันทึกเวอร์ชัน dependency ที่ถูก resolve แล้ว เพื่อช่วยให้แต่ละ
environment ติดตั้ง dependency ชุดเดียวกัน
#3. สร้างโปรเจกต์
สร้าง directory:
mkdir php-unit-testing
cd php-unit-testing
เริ่มต้น Composer:
composer init
หรือสร้าง composer.json เองก็ได้
โครงสร้างที่เราจะใช้คือ:
php-unit-testing/
├── composer.json
├── composer.lock
├── phpunit.xml
├── src/
│ ├── Calculator.php
│ └── DiscountService.php
└── tests/
├── Unit/
│ ├── CalculatorTest.php
│ └── DiscountServiceTest.php
└── Pest.php
#4. กำหนด PSR-4 Autoloading
แก้ไข composer.json:
{
"name": "example/php-unit-testing",
"description": "PHP Unit Testing with PHPUnit and Pest",
"type": "project",
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
จากนั้นสร้าง autoload files:
composer dump-autoload
เมื่อกำหนด namespace เป็น App\\ class ภายใต้ src/ จะสามารถเรียกใช้งานผ่าน
namespace App ได้
#ส่วนที่ 1: PHPUnit
#5. ติดตั้ง PHPUnit
ติดตั้งเป็น development dependency:
composer require --dev phpunit/phpunit
ตรวจสอบ:
./vendor/bin/phpunit --version
บน Windows สามารถใช้:
vendor\bin\phpunit --version
--dev หมายถึง dependency นี้ใช้สำหรับ development/testing และไม่ใช่
dependency หลักของ application runtime
#6. สร้าง Class สำหรับทดสอบ
สร้างไฟล์:
src/Calculator.php
<?php
namespace App;
use InvalidArgumentException;
class Calculator
{
public function add(int $a, int $b): int
{
return $a + $b;
}
public function subtract(int $a, int $b): int
{
return $a - $b;
}
public function multiply(int $a, int $b): int
{
return $a * $b;
}
public function divide(float $a, float $b): float
{
if ($b == 0.0) {
throw new InvalidArgumentException(
'Cannot divide by zero'
);
}
return $a / $b;
}
}
#7. สร้าง Unit Test ด้วย PHPUnit
สร้าง:
tests/Unit/CalculatorTest.php
<?php
use App\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function testAdd(): void
{
$calculator = new Calculator();
$result = $calculator->add(10, 20);
$this->assertSame(30, $result);
}
public function testSubtract(): void
{
$calculator = new Calculator();
$this->assertSame(
5,
$calculator->subtract(10, 5)
);
}
public function testMultiply(): void
{
$calculator = new Calculator();
$this->assertSame(
50,
$calculator->multiply(10, 5)
);
}
public function testDivide(): void
{
$calculator = new Calculator();
$this->assertSame(
5.0,
$calculator->divide(10, 2)
);
}
}
รัน:
./vendor/bin/phpunit tests
หรือเฉพาะไฟล์:
./vendor/bin/phpunit tests/Unit/CalculatorTest.php
#8. แนวคิด Arrange--Act--Assert
Unit Test นิยมแบ่ง logic เป็น 3 ขั้นตอน
#Arrange
เตรียม object และข้อมูล:
$calculator = new Calculator();
#Act
เรียก method ที่ต้องการทดสอบ:
$result = $calculator->add(10, 20);
#Assert
ตรวจสอบผลลัพธ์:
$this->assertSame(30, $result);
เรียกรวมว่า:
Arrange
↓
Act
↓
Assert
หรือ AAA Pattern
#9. PHPUnit Assertions
ตัวอย่าง assertion ที่ใช้บ่อย:
$this->assertSame(10, $value);
$this->assertEquals(10, $value);
$this->assertTrue($result);
$this->assertFalse($result);
$this->assertNull($value);
$this->assertNotNull($value);
$this->assertCount(3, $items);
$this->assertContains('PHP', $languages);
assertSame() ตรวจสอบทั้งค่าและชนิดข้อมูล จึงเหมาะเมื่อ test contract ต้องการ
type ที่แน่นอน
#10. ทดสอบ Exception
เราต้องการให้:
$calculator->divide(10, 0);
โยน InvalidArgumentException
เขียน test:
public function testCannotDivideByZero(): void
{
$calculator = new Calculator();
$this->expectException(
InvalidArgumentException::class
);
$this->expectExceptionMessage(
'Cannot divide by zero'
);
$calculator->divide(10, 0);
}
Unit Test จึงไม่ได้ตรวจเฉพาะ successful path แต่ควรตรวจสอบ error path และ edge cases ด้วย
#ส่วนที่ 2: Pest
#11. Pest คืออะไร
Pest เป็น Testing Framework สำหรับ PHP ที่เน้น syntax กระชับและอ่านง่าย
แทนที่จะเขียน:
class CalculatorTest extends TestCase
{
public function testAdd(): void
{
// ...
}
}
Pest สามารถเขียน:
test('adds two numbers', function () {
// ...
});
และใช้ expectation API เช่น:
expect($result)->toBe(30);
Pest สร้างอยู่บน ecosystem ของ PHPUnit ทำให้แนวคิดและ tooling หลายส่วนสามารถทำงานร่วมกันได้
#12. ติดตั้ง Pest
ติดตั้ง:
composer require --dev pestphp/pest
เริ่มต้น Pest:
./vendor/bin/pest --init
จากนั้นจะมี configuration สำหรับการทดสอบ เช่น:
tests/Pest.php
#13. Unit Test เดิมในรูปแบบ Pest
ตัวอย่าง:
<?php
use App\Calculator;
test('adds two numbers', function () {
$calculator = new Calculator();
expect(
$calculator->add(10, 20)
)->toBe(30);
});
ทดสอบ subtraction:
test('subtracts two numbers', function () {
$calculator = new Calculator();
expect(
$calculator->subtract(10, 5)
)->toBe(5);
});
ทดสอบ multiplication:
test('multiplies two numbers', function () {
$calculator = new Calculator();
expect(
$calculator->multiply(10, 5)
)->toBe(50);
});
ทดสอบ division:
test('divides two numbers', function () {
$calculator = new Calculator();
expect(
$calculator->divide(10, 2)
)->toBe(5.0);
});
รัน:
./vendor/bin/pest
#14. Pest Expectations
ตัวอย่าง:
expect($value)->toBe(10);
expect($name)->toBe('PHP');
expect($result)->toBeTrue();
expect($result)->toBeFalse();
expect($value)->toBeNull();
expect($items)->toHaveCount(3);
สามารถ chain expectations ได้ในกรณีที่เหมาะสม:
expect($name)
->toBeString()
->not->toBeEmpty();
จุดเด่นคือ test อ่านใกล้เคียงภาษาธรรมชาติมากขึ้น
#15. ทดสอบ Exception ด้วย Pest
test('cannot divide by zero', function () {
$calculator = new Calculator();
$calculator->divide(10, 0);
})->throws(
InvalidArgumentException::class,
'Cannot divide by zero'
);
#ส่วนที่ 3: Data-driven Testing
#16. ทำไมต้องใช้ Data-driven Test
สมมติเราต้องทดสอบ:
1 + 2 = 3
5 + 5 = 10
10 + 20 = 30
-5 + 5 = 0
หากสร้าง test แยกทุกกรณีจะเกิด code ซ้ำจำนวนมาก
Pest สามารถเขียนได้:
test(
'adds numbers',
function ($a, $b, $expected) {
$calculator = new Calculator();
expect(
$calculator->add($a, $b)
)->toBe($expected);
}
)->with([
[1, 2, 3],
[5, 5, 10],
[10, 20, 30],
[-5, 5, 0],
]);
แนวคิดคือ:
Test Logic
│
├── Dataset 1
├── Dataset 2
├── Dataset 3
└── Dataset 4
ทำให้เพิ่ม test cases ได้โดยไม่ต้อง duplicate test logic
#ส่วนที่ 4: ทดสอบ Business Logic
#17. สร้าง DiscountService
สร้าง:
src/DiscountService.php
<?php
namespace App;
class DiscountService
{
public function calculate(
float $price,
float $discountPercent
): float {
if ($price < 0) {
throw new \InvalidArgumentException(
'Price cannot be negative'
);
}
if (
$discountPercent < 0 ||
$discountPercent > 100
) {
throw new \InvalidArgumentException(
'Invalid discount percentage'
);
}
return $price -
($price * $discountPercent / 100);
}
}
#18. PHPUnit Test
<?php
use App\DiscountService;
use PHPUnit\Framework\TestCase;
class DiscountServiceTest extends TestCase
{
public function testCalculateDiscount(): void
{
$service = new DiscountService();
$result = $service->calculate(
1000,
10
);
$this->assertSame(
900.0,
$result
);
}
}
#19. Pest Test
<?php
use App\DiscountService;
test(
'calculates discount',
function () {
$service = new DiscountService();
expect(
$service->calculate(1000, 10)
)->toBe(900.0);
}
);
Data-driven version:
test(
'calculates discounts',
function (
$price,
$discount,
$expected
) {
$service = new DiscountService();
expect(
$service->calculate(
$price,
$discount
)
)->toBe($expected);
}
)->with([
[1000.0, 10.0, 900.0],
[1000.0, 20.0, 800.0],
[500.0, 50.0, 250.0],
[100.0, 0.0, 100.0],
]);
#ส่วนที่ 5: Composer Scripts
#20. ลดความยาวของคำสั่งด้วย Composer
ปกติเราอาจเรียก:
./vendor/bin/phpunit
หรือ:
./vendor/bin/pest
สามารถสร้าง Composer Scripts เพื่อให้ทีมใช้คำสั่งมาตรฐานเดียวกันได้
ตัวอย่าง:
{
"scripts": {
"test:phpunit": "phpunit",
"test:pest": "pest",
"test": "pest"
}
}
จากนั้นรัน:
composer test
หรือ:
composer test:phpunit
และ:
composer test:pest
ข้อดีคือ developer และ CI server สามารถใช้คำสั่งเดียวกัน
Developer
│
│ composer test
▼
Composer
│
▼
Testing Framework
│
├── PHPUnit
│
└── Pest
#ส่วนที่ 6: PHPUnit Configuration
#21. สร้าง phpunit.xml
ตัวอย่าง configuration:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
จากนั้นสามารถรัน:
./vendor/bin/phpunit
โดยไม่ต้องระบุ directory ทุกครั้ง
#ส่วนที่ 7: PHPUnit vs Pest
#22. เปรียบเทียบ
หัวข้อ PHPUnit Pest
รูปแบบ Class-based เป็นหลัก Functional/BDD-style
Syntax Explicit กระชับ
Assertions $this->assert...() expect()->to...()
Learning เหมาะกับการเรียนโครงสร้าง xUnit อ่านง่ายและเริ่มต้นเร็ว
Ecosystem Framework หลักของ PHP testing ทำงานบน PHPUnit ecosystem
Existing Projects เหมาะมาก สามารถนำมาใช้เพิ่มได้
Data-driven Test รองรับ เขียนได้กระชับ
CI/CD รองรับ รองรับ
ไม่จำเป็นต้องมองว่า PHPUnit และ Pest เป็นคู่แข่งที่ต้องเลือกเพียงอย่างเดียว เพราะ Pest ใช้ PHPUnit ecosystem อยู่เบื้องหลัง และสามารถค่อย ๆ นำ Pest เข้าไปใช้ใน codebase ที่มี PHPUnit tests อยู่แล้วได้
#ส่วนที่ 8: Unit Test ที่ดีควรเป็นอย่างไร
#23. FIRST Principles
แนวคิดหนึ่งที่ใช้พิจารณา Unit Test คือ FIRST
#Fast
Test ควรรันเร็ว
#Independent
แต่ละ test ไม่ควรพึ่งพากัน
#Repeatable
รันซ้ำแล้วควรได้ผลเหมือนเดิมภายใต้เงื่อนไขเดียวกัน
#Self-validating
Test ต้องตัดสิน PASS/FAIL ได้เอง
#Timely
ควรเขียน test ในช่วงเวลาที่เหมาะสมใกล้กับการพัฒนา feature
#24. สิ่งที่ไม่ควรอยู่ใน Unit Test โดยตรง
Unit Test ควรลด dependency ต่อระบบภายนอก เช่น:
Database
Network
External REST API
File System
Message Broker
Email Server
Cloud Service
หาก class ต้องพึ่ง dependency เหล่านี้ อาจใช้:
- Dependency Injection
- Stub
- Fake
- Mock
เพื่อ isolate unit ที่ต้องการทดสอบ
#ส่วนที่ 9: Workflow ที่แนะนำ
Development workflow สามารถเป็น:
เขียน Requirement
│
▼
สร้าง Class / Function
│
▼
เขียน Unit Test
│
▼
composer test
│
┌───┴───┐
│ │
PASS FAIL
│ │
│ ▼
│ แก้ Code
│ │
│ └────────┐
│ │
▼ │
Refactor ◄──────────┘
│
▼
Commit
│
▼
CI/CD
หากใช้แนวคิด Test-Driven Development สามารถปรับเป็น:
RED
│
│ เขียน Test ที่ยังไม่ผ่าน
▼
GREEN
│
│ เขียน Code ขั้นต่ำให้ Test ผ่าน
▼
REFACTOR
│
│ ปรับปรุงโครงสร้าง Code
└──────────────► ทำซ้ำ
#ส่วนที่ 10: Composer ใน CI/CD
เมื่อ source code ถูก push ไปยัง Git repository CI pipeline สามารถติดตั้ง dependencies:
composer install
แล้วรัน:
composer test
Workflow:
Developer
│
▼
Git Push
│
▼
CI Server
│
├── composer install
│
├── composer test
│
└── Build
│
┌───┴───┐
▼ ▼
PASS FAIL
│
▼
Deploy
สำหรับ CI โดยทั่วไปควร commit composer.lock เพื่อให้ pipeline ติดตั้ง
dependency versions ตามที่โปรเจกต์ล็อกไว้
#สรุป
การสร้างระบบทดสอบสำหรับ PHP สามารถแบ่งหน้าที่ของเครื่องมือได้ดังนี้:
Composer
│
├── Dependency Management
├── Autoloading
└── Test Scripts
│
▼
Test Framework
┌────┴────┐
│ │
PHPUnit Pest
│ │
└────┬────┘
▼
Unit Test
│
┌─────┴─────┐
▼ ▼
PASS FAIL
Composer ดูแล dependencies และ automation commands, PHPUnit เป็น framework หลักที่มีรูปแบบ xUnit ชัดเจน ส่วน Pest เพิ่ม syntax ที่กระชับและ expectation API ที่อ่านง่ายบน PHPUnit ecosystem
สำหรับผู้เริ่มต้น การทดลองเขียน test ชุดเดียวกันทั้ง PHPUnit และ Pest เป็นวิธีที่ดีในการเข้าใจว่า syntax แตกต่างกันอย่างไร ขณะที่หลักการสำคัญยังเหมือนเดิม ได้แก่ การแยกหน่วยที่ต้องการทดสอบ การกำหนด expected behavior และการทำให้ test รันซ้ำได้อย่างสม่ำเสมอ
#คำสั่งสำคัญสรุป
# สร้าง Composer project
composer init
# ติดตั้ง PHPUnit
composer require --dev phpunit/phpunit
# ติดตั้ง Pest
composer require --dev pestphp/pest
# สร้าง autoload
composer dump-autoload
# เริ่มต้น Pest
./vendor/bin/pest --init
# รัน PHPUnit
./vendor/bin/phpunit
# รัน Pest
./vendor/bin/pest
# รันผ่าน Composer Script
composer test
#เอกสารอ้างอิง
- Composer Documentation: https://getcomposer.org/doc/
- PHPUnit Documentation: https://docs.phpunit.de/
- Pest Documentation: https://pestphp.com/docs/
- PHP Documentation: https://www.php.net/docs.php