ป้ายกำกับ: Database Transaction

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 เข้าไปแทน ซึ่งเป็นกุญแจสำคัญในการควบคุมพฤติกรรมของระบบภายใต้การทดสอบ