PlusMagi's Blog By Pitt Phunsanit architecture,Automated Testing,Backend,computer science,Database,Uncategorized PHP: การทำ Integration Testing สำหรับจำลองระบบจริง

PHP: การทำ Integration Testing สำหรับจำลองระบบจริง

  • 1. ใช้ Database Transaction (Transactions for Rollback)
    ในการเขียน IT ที่เกี่ยวข้องกับฐานข้อมูล ควรให้แน่ใจว่าทุกการทดสอบเริ่มต้นด้วยการตั้งค่า BEGIN TRANSACTION และจบลงด้วยการ ROLLBACK เสมอ วิธีนี้ทำให้มั่นใจได้ว่าเมื่อเทสต์เสร็จสิ้น ข้อมูลที่เปลี่ยนแปลงจะถูกย้อนกลับไปสู่สถานะเดิม (Clean Slate) ทำให้ Test Case หนึ่งไม่ส่งผลต่อ Test Case อื่น
  • 2. Dependency Injection และ Mocking
    ใช้หลักการ Dependency Injection (DI) เพื่อให้สามารถ 'ฉีด' (Inject) ตัวจำลอง (Mock Objects) เข้าไปแทนที่ Dependencies จริงๆ ได้ ในตัวอย่างข้างต้น เราไม่ได้เชื่อมต่อกับ PDO จริง แต่เราส่ง $this->pdoMock เข้าไปแทน ซึ่งเป็นกุญแจสำคัญในการควบคุมพฤติกรรมของระบบภายใต้การทดสอบ

  • PHP: การทำ Integration Testing สำหรับจำลองระบบจริง

    ในโลกของการพัฒนาซอฟต์แวร์ยุคใหม่ ความมั่นคงและความน่าเชื่อถือของโค้ด (Reliability) คือสิ่งที่สำคัญที่สุด การเขียน Unit Test เป็นจุดเริ่มต้นที่ยอดเยี่ยมในการตรวจสอบความถูกต้องของฟังก์ชันย่อยๆ แต่เมื่อระบบของเรามีความซับซ้อนขึ้นและประกอบด้วยส่วนประกอบหลายอย่าง เช่น Service Layer, Repository Layer, และการสื่อสารกับฐานข้อมูลภายนอก หรือ API อื่น ๆ แค่การทดสอบเพียงส่วนย่อยก็ไม่เพียงพอ


    1. บทนำและแนวคิดสำคัญ

    Integration Test (IT) คือการทดสอบที่มุ่งเน้นไปที่การตรวจสอบปฏิสัมพันธ์ (Interaction) ระหว่างโมดูลหรือบริการตั้งแต่สองตัวขึ้นไป ว่าเมื่อทำงานร่วมกันแล้วสามารถส่งผ่านข้อมูลและกระบวนการได้สำเร็จตามที่คาดไว้หรือไม่ โดยเป้าหมายคือการจำลองพฤติกรรมของระบบในสภาพแวดล้อมจริงมากที่สุด


    ทำไมต้องใช้ Integration Testing?

    • ปิดช่องว่างของ Unit Test
      Unit Test จะบอกได้ว่า 'โค้ด A' ทำงานถูกต้อง แต่จะไม่รู้เลยว่าเมื่อ 'โค้ด A' คุยกับ 'ฐานข้อมูล B' แล้วเกิดข้อผิดพลาดอะไรขึ้น
    • จำลองโลกจริง (Real-World Simulation)
      IT ช่วยให้มั่นใจได้ว่า Workflow ทั้งหมด—ตั้งแต่การรับ Request $\rightarrow$ การตรวจสอบ Business Logic $\rightarrow$ การบันทึกข้อมูลลง DB และเรียกใช้ภายนอก API—ทำงานอย่างราบรื่นเหมือนกับผู้ใช้งานจริง
    • ค้นหา Error ที่ซ่อนอยู่
      ส่วนใหญ่ของ Bug ในโปรเจกต์ขนาดใหญ่มักจะเกิดจากการเชื่อมต่อ (Integration Point) ไม่ใช่ตัวฟังก์ชันเอง การทดสอบประเภทนี้จึงช่วยจับจุดบกพร่องเหล่านี้ได้

    โดยสรุป Integration Testing จึงเป็นการยืนยันว่า 'ส่วนประกอบทั้งหมดทำงานร่วมกันได้อย่างราบรื่น' ซึ่งเป็นระดับของการทดสอบที่อยู่ระหว่าง Unit Test และ End-to-End (E2E) Testing


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

    ในบทความนี้ เราจะจำลองสถานการณ์ที่ระบบต้องทำหน้าที่ 'สั่งซื้อสินค้า' ซึ่งต้องมีการตรวจสอบผู้ใช้ (User Service) และบันทึกการสั่งซื้อ (Order Service) ลงฐานข้อมูล


    โครงสร้างไฟล์ที่เกี่ยวข้อง

    • App\Service\UserService.php: จัดการ Logic การดึง User
    • App\Repository\OrderRepository.php: จัดการปฏิสัมพันธ์กับ Database (ส่วนนี้คือจุดที่เราต้องการ Integration Test)
    • Tests/Integration/OrderProcessorTest.php: ไฟล์สำหรับเขียน IT

    ตัวอย่าง PHP Code: การทดสอบ Order Repository

// ********************************************
// 1. Class ที่ถูกทดสอบ (App\Repository\OrderRepository.php)
namespace App\Repository;

class OrderRepository {
    private $dbConnection;

    public function __construct(\PDO $dbConnection) {
        $this->dbConnection = $dbConnection;
    }

    /**
     * บันทึกข้อมูลคำสั่งซื้อและตรวจสอบความสมบูรณ์ของ Transaction
     * @param array $orderData ข้อมูลคำสั่งซื้อ
     */
    public function saveOrder(array $orderData): bool {
        try {
            // การเริ่ม Transaction คือสิ่งสำคัญในการ IT กับ DB
            $this->dbConnection->beginTransaction();

            // 1. บันทึกข้อมูลหลัก... (โค้ด SQL)
            $stmt = $this->dbConnection->prepare("INSERT INTO orders (...) VALUES (...)");
            $success = $stmt->execute([/* parameters */]);

            if (!$success) {
                throw new \Exception('Failed to insert order data.');
            }

            // 2. อัพเดทสต็อกสินค้า... (โค้ด SQL อีกส่วน)
            // ...

            $this->dbConnection->commit(); // ถ้าทุกอย่างผ่าน จะ Commit
            return true;
        } catch (\Throwable $e) {
            $this->dbConnection->rollBack(); // ถ้าเกิด Error ใดๆ ต้อง Rollback
            throw new \Exception("Transaction failed: " . $e->getMessage());
        }
    }
}

// ********************************************
// 2. ไฟล์ Integration Test (Tests/Integration/OrderProcessorTest.php)
use PHPUnit\Framework\TestCase;
use App\Repository\OrderRepository;

class OrderProcessorTest extends TestCase {

    private $pdoMock; // Mock สำหรับจำลองการเชื่อมต่อ DB จริงๆ

    protected function setUp(): void {
        parent::setUp();
        // ในโลกจริง เราจะใช้ TestContainer (เช่น Docker) เพื่อให้ได้ DB ที่สะอาดจริงๆ
        $this->pdoMock = $this->createMock(\PDO::class); 
    }

    public function testSuccessfulOrderSubmissionCommitsTransaction() {
        // *** Setup (จำลองสถานะเริ่มต้น) ***
        $orderData = [/* ... */];

        // ********* KEY STEP: การควบคุมการทำงานของ Mocked Dependency ********
        // เราต้องบังคับให้ $this->pdoMock ทำงาน Transaction, Commit และ Rollback ตามลำดับ
        $this->pdoMock->expects($this->once())->method('beginTransaction');
        $this->pdoMock->expects($this->once())->method('commit');
        $this->pdoMock->expects($this->never())->method('rollBack'); // ต้องมั่นใจว่าจะไม่ Rollback ถ้าสำเร็จ

        // ********* Action (เรียก Service ที่มีการพึ่งพา DB) ********
        $repository = new OrderRepository($this->pdoMock);
        $result = $repository->saveOrder($orderData);

        // ********* Assertion (ตรวจสอบผลลัพธ์) ********
        $this->assertTrue($result, "การบันทึกคำสั่งซื้อควรจะสำเร็จและ Commit Transaction");
    }

    public function testFailedDatabaseConnectionTriggersRollback() {
        // *** Setup: จำลองให้เกิด Exception ในระหว่างกระบวนการ ***
        $orderData = [/* ... */];
        
        // บังคับให้ครั้งแรกที่รันคือ begin, แต่เมื่อถึงจุดใดจุดหนึ่ง (สมมติว่าเป็น $stmt->execute) ให้เกิด Error 
        // และเราต้องตรวจสอบว่า Rollback ถูกเรียกใช้จริงหรือไม่
        $this->pdoMock->expects($this->once())->method('beginTransaction');
        $this->pdoMock->expects($this->never())->method('commit'); 
        $this->pdoMock->expects($this->once())->method('rollBack'); // ต้องมั่นใจว่าเกิด Error แล้ว Rollback

        $repository = new OrderRepository($this->pdoMock);

        // ********* Act & Assert: คาดหวังให้ Exception ถูกโยนออกมา ********
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("Transaction failed");
        
        $repository->saveOrder($orderData); 
    }
}

คำอธิบายโค้ดสำคัญ
จุดที่ทำให้การทดสอบนี้เป็น 'Integration Test' คือเราไม่ได้แค่เรียกฟังก์ชัน แต่เราควบคุมและตรวจสอบว่าลำดับของเมธอดของ Object ที่เกี่ยวข้อง (beginTransaction() $\rightarrow$ execute() $\rightarrow$ commit() หรือ rollBack()) ถูกเรียกใช้ตามตรรกะทางธุรกิจหรือไม่ ซึ่งเป็นกระบวนการที่จำลอง Transaction ของระบบจริง


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

แม้ว่า Integration Testing จะมีประโยชน์อย่างมหาศาล แต่หากทำไม่ถูกวิธีก็จะกลายเป็น 'Test Burden' ที่ทำให้การพัฒนาช้าลงได้ นี่คือสิ่งที่ควรรู้:


⚠️ ปัญหาที่มักพบ (Pitfalls)

  • Over-Testing
    อย่าพยายามใช้ Integration Test เพื่อทดสอบ Logic ภายในของฟังก์ชันย่อยๆ ทั้งหมด ให้สงวน IT ไว้เพื่อทดสอบ 'เส้นทาง' การไหลของข้อมูลระหว่างส่วนประกอบเท่านั้น
  • Slow Test Suite (ชุดทดสอบที่ช้า)
    เนื่องจาก IT ต้องมีการเชื่อมต่อภายนอก (DB, API) ทำให้ใช้เวลานานมาก หากมีไฟล์ทดสอบเป็นร้อยๆ ไฟล์ การรันทั้งหมดทุกครั้งจะทำให้ Cycle Time ของนักพัฒนาสูงเกินไป
  • Global State Dependency
    หากโค้ดถูกออกแบบให้พึ่งพา Global Variables หรือ Static Methods มากเกินไป การเขียน IT จะยุ่งยากและยากต่อการ Mocking/Isolation ทำให้ควรบังคับใช้หลักการ Clean Architecture ตั้งแต่ต้น

✨ Best Practices สำหรับการทำ Integration Test

  • 1. ใช้ Database Transaction (Transactions for Rollback)
    ในการเขียน IT ที่เกี่ยวข้องกับฐานข้อมูล ควรให้แน่ใจว่าทุกการทดสอบเริ่มต้นด้วยการตั้งค่า BEGIN TRANSACTION และจบลงด้วยการ ROLLBACK เสมอ วิธีนี้ทำให้มั่นใจได้ว่าเมื่อเทสต์เสร็จสิ้น ข้อมูลที่เปลี่ยนแปลงจะถูกย้อนกลับไปสู่สถานะเดิม (Clean Slate) ทำให้ Test Case หนึ่งไม่ส่งผลต่อ Test Case อื่น
  • 2. Dependency Injection และ Mocking
    ใช้หลักการ Dependency Injection (DI) เพื่อให้สามารถ 'ฉีด' (Inject) ตัวจำลอง (Mock Objects) เข้าไปแทนที่ Dependencies จริงๆ ได้ ในตัวอย่างข้างต้น เราไม่ได้เชื่อมต่อกับ PDO จริง แต่เราส่ง $this->pdoMock เข้าไปแทน ซึ่งเป็นกุญแจสำคัญในการควบคุมพฤติกรรมของระบบภายใต้การทดสอบ
Exit mobile version