#การทดสอบระดับหน่วยด้วย PHPUnit และ Pest พร้อมติดตั้ง Testing Library ด้วย Composer

Unit Testing หรือ การทดสอบระดับหน่วย เป็นการตรวจสอบส่วนย่อยของโปรแกรม เช่น method, function หรือ class แบบแยกจากระบบส่วนอื่นให้มากที่สุด จุดประสงค์คือยืนยันว่า logic ของโปรแกรมทำงานตรงตามที่ออกแบบไว้ และช่วยตรวจจับ regression เมื่อมีการแก้ไขโค้ดในอนาคต

สำหรับ PHP เครื่องมือที่นิยมมากคือ:

  • PHPUnit — Testing Framework มาตรฐานของโลก PHP
  • Pest — Testing Framework ที่มี syntax กระชับ อ่านง่าย และทำงานอยู่บนพื้นฐานของ PHPUnit
  • Composer — Dependency Manager ที่ใช้ติดตั้ง PHPUnit, Pest และ library อื่นของโปรเจกต์

ณ วันที่ 21 กันยายน 2026 เอกสาร PHPUnit 12 ระบุว่าต้องใช้ PHP 8.3+ ขณะที่ Pest รุ่นปัจจุบันระบุ PHP 8.4+ ดังนั้นหากต้องการทดลองทั้ง PHPUnit และ Pest แนะนำให้ใช้ PHP 8.4 ขึ้นไป


#1. Unit Test คืออะไร

สมมติว่าระบบมี class สำหรับคำนวณราคา:

<?php

namespace App;

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

Unit Test จะทดสอบเฉพาะพฤติกรรมของ Calculator เช่น

2 + 3 ต้องได้ 5
-1 + 1 ต้องได้ 0

โดยไม่จำเป็นต้องเปิด Web Server, Database หรือ Browser

แนวคิดหลักคือ

Input
  ↓
Unit Under Test
  ↓
Actual Result
  ↓
Assertion
  ↓
Expected Result

#2. PHPUnit และ Pest ต่างกันอย่างไร

หัวข้อ PHPUnit Pest
รูปแบบ Class-based Functional / BDD-style
Syntax เป็นทางการ กระชับ
ความนิยม มาตรฐาน PHP นิยมมากใน PHP/Laravel
Assertion $this->assertEquals() expect()->toBe()
Data-driven Data Provider Dataset
ทำงานบน PHPUnit ใช่
เหมาะกับ โปรเจกต์ทั่วไป, Legacy, Enterprise โปรเจกต์ใหม่, Laravel, ทีมที่ต้องการ test อ่านง่าย

Pest ไม่ได้ทิ้ง PHPUnit แต่ใช้ PHPUnit เป็น foundation ดังนั้นแนวคิดเรื่อง assertion, lifecycle และ test runner จำนวนมากยังเชื่อมโยงกับ PHPUnit


#3. ตรวจสอบ PHP และ Composer

ก่อนเริ่มให้ตรวจสอบ PHP

php -v

ตัวอย่าง

PHP 8.4.x

ตรวจสอบ Composer

composer --version

ถ้ายังไม่มี Composer ให้ติดตั้งจากเว็บไซต์ Composer ก่อน


#4. สร้าง PHP Project

สร้าง directory

mkdir php-unit-testing
cd php-unit-testing

สร้าง composer.json

composer init

หรือสร้างไฟล์เอง

{
    "name": "demo/php-unit-testing",
    "description": "PHP Unit Testing with PHPUnit and Pest",
    "type": "project",
    "require": {
        "php": "^8.4"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    }
}

จากนั้นสร้าง autoload

composer dump-autoload

#5. ทำไม Testing Library ควรอยู่ใน require-dev

PHPUnit และ Pest ใช้ตอนพัฒนาและทดสอบ ไม่ใช่ dependency ที่ application ต้องใช้ขณะทำงานจริง

จึงควรติดตั้งด้วย

composer require --dev ...

หรือ

composer require ... --dev

Composer จะบันทึก package เหล่านี้ไว้ใน

{
    "require-dev": {
    }
}

เวลา deploy production สามารถไม่ติดตั้ง development dependencies ได้ด้วย

composer install --no-dev

#6. วิธีที่ 1: ติดตั้ง PHPUnit ด้วย Composer

ติดตั้ง PHPUnit แบบ project-local

composer require --dev phpunit/phpunit

ตรวจสอบ version

./vendor/bin/phpunit --version

บน Windows สามารถใช้

vendor\bin\phpunit --version

ข้อดีของการติดตั้งแบบ project-local คือแต่ละ project สามารถกำหนด PHPUnit version ของตัวเองได้ ไม่ต้องพึ่ง global installation


#7. สร้างโครงสร้างโปรเจกต์

สร้าง directory

php-unit-testing/
├── composer.json
├── composer.lock
├── src/
│   └── Calculator.php
├── tests/
│   └── Unit/
│       └── CalculatorTest.php
└── vendor/

ไฟล์ 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(
                'Division by zero is not allowed'
            );
        }

        return $a / $b;
    }
}

หลังเพิ่ม class แล้วสามารถ refresh autoload ด้วย

composer dump-autoload

#8. เขียน Unit Test ด้วย PHPUnit

สร้างไฟล์

tests/Unit/CalculatorTest.php
<?php

namespace Tests\Unit;

use App\Calculator;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function test_add_two_numbers(): void
    {
        $calculator = new Calculator();

        $result = $calculator->add(2, 3);

        $this->assertSame(5, $result);
    }

    public function test_subtract_two_numbers(): void
    {
        $calculator = new Calculator();

        $result = $calculator->subtract(10, 4);

        $this->assertSame(6, $result);
    }

    public function test_multiply_two_numbers(): void
    {
        $calculator = new Calculator();

        $result = $calculator->multiply(4, 5);

        $this->assertSame(20, $result);
    }
}

รูปแบบพื้นฐานของ Unit Test มักประกอบด้วย

Arrange → Act → Assert

ตัวอย่าง

// Arrange
$calculator = new Calculator();

// Act
$result = $calculator->add(2, 3);

// Assert
$this->assertSame(5, $result);

#9. รัน PHPUnit

รันทั้งหมด

./vendor/bin/phpunit tests

รันเฉพาะ Unit Test

./vendor/bin/phpunit tests/Unit

รันเฉพาะไฟล์

./vendor/bin/phpunit tests/Unit/CalculatorTest.php

รันเฉพาะ test ที่ชื่อกำหนด

./vendor/bin/phpunit --filter test_add_two_numbers

#10. Assertion ที่ใช้บ่อยใน PHPUnit

#assertSame

ตรวจทั้ง value และ type

$this->assertSame(5, $result);

#assertEquals

ตรวจค่าที่เทียบเท่ากัน

$this->assertEquals(5, $result);

#assertTrue

$this->assertTrue($result);

#assertFalse

$this->assertFalse($result);

#assertNull

$this->assertNull($result);

#assertCount

$this->assertCount(3, $items);

#assertContains

$this->assertContains('PHP', $languages);

#assertInstanceOf

$this->assertInstanceOf(Calculator::class, $calculator);

#11. ทดสอบ Exception ด้วย PHPUnit

กรณีหารด้วยศูนย์

public function test_divide_by_zero_throws_exception(): void
{
    $calculator = new Calculator();

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('Division by zero is not allowed');

    $calculator->divide(10, 0);
}

test จะผ่านเมื่อ method โยน exception ตามที่คาดไว้


#12. Data Driven Testing ด้วย PHPUnit

หากต้องทดสอบ input หลายชุด ไม่ควรเขียน test ซ้ำจำนวนมาก

PHPUnit สามารถใช้ Data Provider ได้

<?php

namespace Tests\Unit;

use App\Calculator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class CalculatorDataTest extends TestCase
{
    public static function additionProvider(): array
    {
        return [
            'positive numbers' => [2, 3, 5],
            'zero'             => [0, 0, 0],
            'negative numbers' => [-2, -3, -5],
            'mixed numbers'    => [-2, 5, 3],
        ];
    }

    #[DataProvider('additionProvider')]
    public function test_add(
        int $a,
        int $b,
        int $expected
    ): void {
        $calculator = new Calculator();

        $this->assertSame(
            $expected,
            $calculator->add($a, $b)
        );
    }
}

#13. PHPUnit Configuration

สามารถสร้างไฟล์ phpunit.xml

<?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

#14. วิธีที่ 2: ติดตั้ง Pest ด้วย Composer

Pest ทำงานบน PHPUnit แต่เอกสาร Pest แนะนำว่า ถ้า project มี phpunit/phpunit เป็น direct dependency อยู่ก่อน ให้ถอด direct dependency นั้นออก แล้วให้ Pest จัดการ PHPUnit version ที่เข้ากันได้

composer remove phpunit/phpunit

ติดตั้ง Pest

composer require pestphp/pest --dev --with-all-dependencies

ระหว่างติดตั้ง Composer อาจขออนุญาตใช้งาน Composer plugin ของ Pest ให้ตรวจสอบ package แล้วอนุญาตตามขั้นตอนของ Composer

initialize Pest

./vendor/bin/pest --init

ระบบจะเตรียมโครงสร้าง test และไฟล์ configuration เช่น

tests/
├── Pest.php
├── Unit/
│   └── ExampleTest.php
└── Feature/
    └── ExampleTest.php

รัน Pest

./vendor/bin/pest

#15. เขียน Unit Test ด้วย Pest

สร้างไฟล์

tests/Unit/CalculatorTest.php
<?php

use App\Calculator;

test('add two numbers', function () {
    $calculator = new Calculator();

    $result = $calculator->add(2, 3);

    expect($result)->toBe(5);
});

หรือใช้รูปแบบ it()

<?php

use App\Calculator;

it('adds two numbers', function () {
    $calculator = new Calculator();

    expect(
        $calculator->add(2, 3)
    )->toBe(5);
});

#16. Pest Expectation

Pest ใช้ syntax แบบ chain ทำให้อ่านคล้ายภาษาธรรมชาติ

#toBe

expect($result)->toBe(5);

#toEqual

expect($result)->toEqual(5);

#toBeTrue

expect($result)->toBeTrue();

#toBeFalse

expect($result)->toBeFalse();

#toBeNull

expect($result)->toBeNull();

#toHaveCount

expect($items)->toHaveCount(3);

#toContain

expect($languages)->toContain('PHP');

#toBeInstanceOf

expect($calculator)->toBeInstanceOf(Calculator::class);

สามารถ chain expectation ได้

expect($result)
    ->toBeInt()
    ->toBeGreaterThan(0);

#17. ทดสอบหลาย Method ด้วย Pest

<?php

use App\Calculator;

beforeEach(function () {
    $this->calculator = new Calculator();
});

it('adds numbers', function () {
    expect(
        $this->calculator->add(2, 3)
    )->toBe(5);
});

it('subtracts numbers', function () {
    expect(
        $this->calculator->subtract(10, 4)
    )->toBe(6);
});

it('multiplies numbers', function () {
    expect(
        $this->calculator->multiply(4, 5)
    )->toBe(20);
});

beforeEach() จะทำงานก่อน test แต่ละตัว


#18. ทดสอบ Exception ด้วย Pest

<?php

use App\Calculator;
use InvalidArgumentException;

it('throws exception when dividing by zero', function () {
    $calculator = new Calculator();

    $calculator->divide(10, 0);
})->throws(
    InvalidArgumentException::class,
    'Division by zero is not allowed'
);

#19. Data Driven Testing ด้วย Pest Dataset

Pest สามารถสร้าง Dataset ได้

<?php

use App\Calculator;

dataset('addition cases', [
    [2, 3, 5],
    [0, 0, 0],
    [-2, -3, -5],
    [-2, 5, 3],
]);

it(
    'adds numbers correctly',
    function (int $a, int $b, int $expected) {
        $calculator = new Calculator();

        expect(
            $calculator->add($a, $b)
        )->toBe($expected);
    }
)->with('addition cases');

รูปแบบนี้เหมาะกับ test ที่มี test data หลายชุด


#20. รัน Pest

รัน test ทั้งหมด

./vendor/bin/pest

รันเฉพาะ Unit tests

./vendor/bin/pest tests/Unit

รันเฉพาะไฟล์

./vendor/bin/pest tests/Unit/CalculatorTest.php

filter ตามชื่อ

./vendor/bin/pest --filter "adds two numbers"

แสดงรายละเอียดเพิ่ม

./vendor/bin/pest --verbose

#21. Pest ยังใช้ PHPUnit Assertion ได้

เนื่องจาก Pest ทำงานบน PHPUnit จึงสามารถใช้ PHPUnit assertion ภายใน Pest test ได้ด้วย

it('can use PHPUnit assertion', function () {
    $result = 2 + 3;

    $this->assertSame(5, $result);
});

แต่หากเลือก Pest โดยทั่วไปจะนิยมเขียน

expect($result)->toBe(5);

เพื่อให้รูปแบบ test สม่ำเสมอและอ่านง่าย


#22. เพิ่ม Composer Scripts

สามารถเพิ่ม script ใน composer.json

กรณี PHPUnit

{
    "scripts": {
        "test": "phpunit"
    }
}

รัน

composer test

กรณี Pest

{
    "scripts": {
        "test": "pest"
    }
}

แล้วใช้

composer test

ข้อดีคือ developer และ CI ใช้ command เดียวกัน


#23. ตัวอย่าง composer.json สำหรับ PHPUnit

{
    "name": "demo/phpunit-example",
    "require": {
        "php": "^8.4"
    },
    "require-dev": {
        "phpunit/phpunit": "^12.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "phpunit"
    }
}

หลังแก้ autoload

composer dump-autoload

ทดสอบ

composer test

#24. ตัวอย่าง composer.json สำหรับ Pest

การติดตั้งจริงแนะนำให้ใช้ Composer command เพื่อให้ Composer เลือก Pest/ PHPUnit dependencies ที่ compatible กับ environment

composer require pestphp/pest --dev --with-all-dependencies

ตัวอย่างโครงสร้างไฟล์หลังติดตั้งอาจมีลักษณะ

{
    "name": "demo/pest-example",
    "require": {
        "php": "^8.4"
    },
    "require-dev": {
        "pestphp/pest": "^5.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "pest"
    }
}

ไม่ควร copy version constraint โดยไม่ตรวจสอบ PHP version และ compatibility ของ project เดิม ให้ Composer เป็นผู้ resolve dependency


#25. ไม่ควรติดตั้ง PHPUnit และ Pest แบบ direct dependency พร้อมกันโดยไม่จำเป็น

แนวคิดที่มักทำให้สับสนคือ

composer require --dev phpunit/phpunit
composer require --dev pestphp/pest

Pest เองมี dependency ต่อ PHPUnit และต้องการ PHPUnit version ที่ compatible กับ Pest version นั้น

ดังนั้นแนวทางที่ปลอดภัยคือเลือกหนึ่งในสองรูปแบบ

#ใช้ PHPUnit เป็นหลัก

composer require --dev phpunit/phpunit

#ใช้ Pest เป็นหลัก

หากเคยติดตั้ง PHPUnit โดยตรง

composer remove phpunit/phpunit

แล้ว

composer require pestphp/pest --dev --with-all-dependencies
./vendor/bin/pest --init

ถึงใช้ Pest ก็ยังสามารถรัน PHPUnit-style test cases ได้ เพราะ Pest ทำงานบน PHPUnit


#26. โครงสร้าง Test ที่แนะนำ

project/
├── src/
│   ├── Calculator.php
│   ├── PriceService.php
│   └── OrderService.php
│
├── tests/
│   ├── Unit/
│   │   ├── CalculatorTest.php
│   │   ├── PriceServiceTest.php
│   │   └── OrderServiceTest.php
│   │
│   ├── Feature/
│   └── Pest.php
│
├── composer.json
├── composer.lock
├── phpunit.xml
└── vendor/

สำหรับ Unit Test ควรเน้น class หรือ business logic ที่สามารถทดสอบแบบ isolated ได้


#27. สิ่งที่ Unit Test ไม่ควรทำมากเกินไป

Unit Test ควรเร็วและแยกจาก dependency ภายนอก

หลีกเลี่ยงการให้ Unit Test ติดต่อโดยตรงกับ

Database จริง
External API จริง
Filesystem จริง
Message Broker จริง
Web Browser
Network Service

dependency เหล่านี้ควร mock, fake, stub หรือย้ายไปทดสอบใน Integration / Feature Test ตามวัตถุประสงค์


#28. หลัก AAA: Arrange – Act – Assert

รูปแบบที่ช่วยให้ test อ่านง่ายคือ

Arrange
  เตรียม object และ input

Act
  เรียก method ที่ต้องการทดสอบ

Assert
  ตรวจสอบผลลัพธ์

ตัวอย่าง PHPUnit

public function test_add(): void
{
    // Arrange
    $calculator = new Calculator();

    // Act
    $result = $calculator->add(2, 3);

    // Assert
    $this->assertSame(5, $result);
}

ตัวอย่าง Pest

it('adds numbers', function () {
    // Arrange
    $calculator = new Calculator();

    // Act
    $result = $calculator->add(2, 3);

    // Assert
    expect($result)->toBe(5);
});

#29. แนวทางตั้งชื่อ Unit Test

ชื่อ test ควรบอก behavior ที่คาดหวัง

ตัวอย่าง PHPUnit

test_add_returns_sum_of_two_numbers
test_divide_by_zero_throws_exception
test_discount_is_applied_for_premium_customer

Pest สามารถเขียนเป็นภาษาธรรมชาติ

it('returns the sum of two numbers', ...)
it('throws an exception when dividing by zero', ...)
it('applies discount for premium customers', ...)

test ที่ชื่อชัดเจนจะช่วยให้หา defect ได้เร็วเมื่อ CI แจ้ง failure


#30. Unit Test ที่ดีควรมีคุณสมบัติ FIRST

แนวคิด FIRST ประกอบด้วย

  • Fast — รันเร็ว
  • Independent — test ไม่ขึ้นต่อกัน
  • Repeatable — รันซ้ำได้ผลเหมือนเดิม
  • Self-validating — ตัดสิน pass/fail ได้อัตโนมัติ
  • Timely — เขียนใกล้กับช่วงเวลาที่พัฒนา feature

#31. PHPUnit vs Pest: ควรเลือกอะไร

#เลือก PHPUnit เมื่อ

  • project เดิมมี PHPUnit อยู่แล้ว
  • ทีมคุ้นกับ xUnit style
  • ต้องการ class-based tests
  • มี test suite ขนาดใหญ่ที่สร้างด้วย PHPUnit
  • ต้องการใช้ PHPUnit API โดยตรง

#เลือก Pest เมื่อ

  • เริ่ม PHP project ใหม่
  • ต้องการ syntax ที่สั้นและอ่านง่าย
  • ใช้ Laravel หรือ ecosystem ที่รองรับ Pest ดี
  • ทีมชอบ BDD-style test
  • ต้องการ Dataset และ expectation syntax ที่กระชับ

#32. Workflow ที่แนะนำ

Developer
   │
   ▼
เขียน Production Code
   │
   ▼
เขียน Unit Test
   │
   ├── PHPUnit
   │      หรือ
   └── Pest
   │
   ▼
composer test
   │
   ▼
PASS / FAIL
   │
   ▼
Git Commit
   │
   ▼
CI Pipeline
   │
   ▼
Automated Test

#33. ตัวอย่างการใช้งานกับ GitHub Actions

ไฟล์

.github/workflows/test.yml
name: PHP Unit Test

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run tests
        run: composer test

เมื่อ push หรือสร้าง pull request ระบบจะติดตั้ง dependency และรัน Unit Test อัตโนมัติ


#34. คำสั่งสำคัญสรุป

#PHPUnit

composer require --dev phpunit/phpunit
./vendor/bin/phpunit --version
./vendor/bin/phpunit
./vendor/bin/phpunit tests/Unit
./vendor/bin/phpunit --filter test_name

#Pest

composer remove phpunit/phpunit
composer require pestphp/pest --dev --with-all-dependencies
./vendor/bin/pest --init
./vendor/bin/pest
./vendor/bin/pest tests/Unit
./vendor/bin/pest --filter "test name"

#Composer

composer install
composer update
composer dump-autoload
composer test
composer install --no-dev

#35. สรุป

การทำ Unit Testing ใน PHP สามารถเริ่มต้นได้ง่ายด้วย Composer

หากใช้ PHPUnit:

composer require --dev phpunit/phpunit

ถ้าต้องการ syntax ที่กระชับขึ้นสามารถใช้ Pest:

composer require pestphp/pest --dev --with-all-dependencies
./vendor/bin/pest --init

PHPUnit เหมาะกับผู้ที่ต้องการ framework มาตรฐานและรูปแบบ xUnit แบบชัดเจน ส่วน Pest เหมาะกับผู้ที่ต้องการ syntax แบบ modern, expressive และอ่านง่าย โดย Pest ยังคงใช้ PHPUnit เป็นพื้นฐาน

สำหรับทีมพัฒนา สิ่งที่สำคัญกว่าการเลือกว่า PHPUnit หรือ Pest คือการสร้าง test ที่

เร็ว
แยกจาก dependency ภายนอก
อ่านง่าย
รันซ้ำได้
และนำไปรันอัตโนมัติใน CI

เมื่อ Unit Test กลายเป็นส่วนหนึ่งของ development workflow ทีมจะสามารถ refactor, เพิ่ม feature และแก้ไข bug ได้อย่างมั่นใจมากขึ้น


#เอกสารอ้างอิง