PHP: การเขียน Unit Testing ด้วย PHPUnit

ในฐานะนักพัฒนา PHP ระดับ Senior การเขียนโค้ดที่ทำงานได้ (Functional) นั้นเป็นเพียงครึ่งทางเท่านั้น โค้ดที่ดีต้องเป็นโค้ดที่สามารถบำรุงรักษาได้ (Maintainable) และเชื่อถือได้ (Reliable) ซึ่งหัวใจสำคัญของการทำให้โค้ดมีคุณภาพสูงคือการทำ Unit Testing

Unit Test คืออะไร? มันคือกระบวนการทดสอบส่วนประกอบย่อยที่สุดของระบบ (The smallest isolated unit of code) เช่น ฟังก์ชัน, เมธอด (Method), หรือคลาสหนึ่งๆ โดยที่ Unit นั้นจะต้องถูกแยกออกจาก Dependencies ภายนอกทั้งหมด ไม่ว่าจะเป็นฐานข้อมูล, ระบบไฟล์, หรือ API ภายนอก เพื่อให้แน่ใจว่าเมื่อเราเปลี่ยนแปลงโค้ดส่วนใด ส่วนอื่นจะไม่พังตามไปด้วย

PHPUnit คือ Framework มาตรฐานสำหรับการทำ Unit Testing ในภาษา PHP มันเป็นเครื่องมือที่ช่วยให้เราสามารถเขียน Test Case ได้อย่างมีโครงสร้างและรันการทดสอบทั้งหมดได้อย่างอัตโนมัติ แนวคิดหลักของการใช้ PHPUnit ไม่ใช่แค่การ “ตรวจสอบว่าโค้ดทำงานได้” แต่คือการสร้าง Safety Net ให้กับโปรเจกต์ของเรา เมื่อใดก็ตามที่เราต้องการ Refactor (ปรับปรุงโครงสร้าง) โค้ด เราเพียงแค่รัน Test Suite หากมี Unit ใดที่พัง นั่นหมายความว่าเราทำลาย Behavior เดิมของระบบไปแล้ว

การใช้ Unit Testing ยังสอดคล้องกับหลักการ Test-Driven Development (TDD) ซึ่งเป็นแนวทางที่นักพัฒนาจะเขียน Test Case ก่อน จากนั้นจึงค่อยเขียนโค้ดจริงให้ผ่าน Test นั้นๆ ทำให้เราได้ออกแบบ API และ Class ให้มีความชัดเจนและทดสอบได้ตั้งแต่ต้น


ตัวอย่างการใช้งานและรูปแบบโค้ด (Code Examples)

เราจะใช้ตัวอย่างคลาสคำนวณพื้นฐาน (Calculator) เพื่อสาธิตกระบวนการเขียน Unit Test โดยสมมติว่าเรามีไฟล์ src/Calculator.php และเราต้องการสร้าง Test Case ในไฟล์ tests/CalculatorTest.php

หลักการทำงาน
เราจะทดสอบเมธอด add() ว่าเมื่อรับค่าตัวเลขสองตัวมา จะต้องคืนค่าผลรวมที่ถูกต้องเสมอ

// ------------------------------------------------------
// 1. System Under Test (SUT): ไฟล์ src/Calculator.php
// คลาสนี้คือโค้ดที่เราต้องการทดสอบ
// ------------------------------------------------------

namespace App\Service;

class Calculator {
    /**
     * คำนวณผลรวมของตัวเลขสองจำนวน
     * @param float $a ตัวเลขตัวแรก
     * @param float $b ตัวเลขตัวที่สอง
     * @return float ผลรวม
     */
    public function add(float $a, float $b): float {
        // โค้ดนี้คือ Unit ที่เราต้องการทดสอบความถูกต้อง
        return $a + $b;
    }

    /**
     * คำนวณผลต่างของตัวเลขสองจำนวน
     */
    public function subtract(float $a, float $b): float {
        return $a - $b;
    }
}


// ------------------------------------------------------
// 2. Unit Test Case: ไฟล์ tests/CalculatorTest.php
// คลาสนี้คือโค้ดที่ใช้ PHPUnit ในการตรวจสอบ Calculator
// ------------------------------------------------------

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

/**
 * @covers \App\Service\Calculator
 */
class CalculatorTest extends TestCase {

    private Calculator $calculator;

    /**
     * Setup: เมธอดนี้จะถูกเรียกก่อนการรัน Test Case แต่ละครั้ง
     */
    protected function setUp(): void {
        parent::setUp();
        // สร้าง Instance ของคลาสที่เราต้องการทดสอบ
        $this->calculator = new Calculator();
    }

    /**
     * Test Case 1: ทดสอบเมธอด add() ด้วยค่าบวก
     * @test
     */
    public function testAddPositiveNumbers(): void {
        // Arrange (เตรียมข้อมูล)
        $a = 5.0;
        $b = 3.0;
        $expectedResult = 8.0;

        // Act (เรียกใช้ Unit ที่ต้องการทดสอบ)
        $actualResult = $this->calculator->add($a, $b);

        // Assert (ยืนยันผลลัพธ์) - PHPUnit Assertion Method
        $this->assertEquals($expectedResult, $actualResult, "การบวกค่าบวกควรได้ผลรวมที่ถูกต้อง");
    }

    /**
     * Test Case 2: ทดสอบเมธอด add() ด้วยค่าติดลบ
     * @test
     */
    public function testAddNegativeNumbers(): void {
        // Arrange
        $a = -10.0;
        $b = 5.0;
        $expectedResult = -5.0;

        // Act
        $actualResult = $this->calculator->add($a, $b);

        // Assert
        $this->assertEquals($expectedResult, $actualResult);
    }

    /**
     * Test Case 3: ทดสอบเมธอด subtract() ว่าทำงานได้ถูกต้องหรือไม่
     */
    public function testSubtractFunctionality(): void {
        // Arrange & Act
        $result = $this->calculator->subtract(10.0, 4.0);

        // Assert
        $this->assertEquals(6.0, $result);
    }
}


ข้อควรระวัง Security และ Best Practices

  • คำนึงถึงความปลอดภัย (Security)
    ในการเขียน Unit Test เราต้องมั่นใจว่าเราไม่ได้ Hardcode ข้อมูลลับ (Secrets) เช่น API Keys หรือรหัสผ่านลงในไฟล์ Test Case โดยเด็ดขาด หากจำเป็นต้องใช้ข้อมูลเหล่านี้ ควรใช้ Environment Variables (`$_ENV` หรือ `getenv()`) และควรจำลอง (Mock) การเรียกใช้งาน Service ที่เกี่ยวข้องกับฐานข้อมูลหรือระบบภายนอกเสมอ เพื่อป้องกันการรั่วไหลของข้อมูลจริง
  • Performance
    Unit Test ไม่ควรช้าเกินไป เพราะนักพัฒนาจะเริ่มขี้เกียจรันมัน วิธีแก้คือการใช้เทคนิค Mocking และ Stubbing อย่างเคร่งครัด เมื่อเราทดสอบคลาส A ที่เรียกใช้ Service B (เช่น Database Connection) เราไม่ควรให้โค้ดจริงเชื่อมต่อกับฐานข้อมูล แต่ควรสร้าง Mock Object ของ Service B ขึ้นมาแทน เพื่อให้ Test Case ทำงานในหน่วยความจำเท่านั้น ทำให้รวดเร็วและเป็นอิสระ
  • Error Handling
    แทนที่จะใช้ try...catch ในการเขียน Unit Test (ซึ่งเป็นการจัดการ Runtime Error) ควรใช้ Assertion Methods ที่ PHPUnit มีให้ เช่น $this->expectException(\InvalidArgumentException::class); การทำเช่นนี้จะทำให้ Test Case ของเรามีความชัดเจนว่า “เราคาดหวังให้เกิด Exception นี้” ซึ่งเป็นแนวทางที่ถูกต้องตามหลักการ Testing

สรุปและการนำไปประยุกต์ใช้งาน

การทำ Unit Testing ไม่ใช่แค่ “ฟีเจอร์เสริม” แต่เป็นส่วนหนึ่งของโครงสร้างพื้นฐาน (Infrastructure) ของโปรเจกต์ที่ทันสมัย การลงทุนเวลาในการเขียน Test Case ในช่วงแรก อาจดูเหมือนเสียเวลา แต่ในระยะยาว มันจะช่วยลดค่าใช้จ่ายในการแก้ไขบั๊ก (Bug Fixing Cost) ได้อย่างมหาศาล

คำแนะนำสำหรับการนำไปใช้งานจริง (Production)

  • Integration into CI/CD
    ต้องกำหนดให้การรัน Test Suite เป็นขั้นตอนบังคับก่อนที่จะมีการ Deploy โค้ดใดๆ เข้าสู่ Staging หรือ Production Environment
  • Code Coverage Tools
    ใช้เครื่องมืออย่าง PHP Code Coverage เพื่อวัดว่าโค้ดส่วนไหนของเราที่ยังไม่ถูกทดสอบ (Uncovered Lines) และนำข้อมูลนั้นมาปรับปรุง Test Case ให้ครอบคลุมมากขึ้น

การมี Unit Tests ที่แข็งแกร่งจะทำให้ทีมสามารถทำงานร่วมกันได้อย่างมั่นใจ สามารถ Refactor โค้ดครั้งใหญ่ได้โดยไม่ต้องกลัวว่าระบบหลักจะล่ม ซึ่งนี่คือคุณค่าที่แท้จริงของ Senior Developer ในยุคสมัยของการพัฒนาซอฟต์แวร์ที่รวดเร็วและเปลี่ยนแปลงตลอดเวลา

Exit mobile version