Skip to main content

เนื้อหาภาษาไทย / Thai content

System guide Content reviewed

Mocking คืออะไร: การจำลอง Dependencies ใน Unit Testing

บทความนี้อธิบายถึงความจำเป็นของการใช้ Mocking ใน Unit Testing เพื่อจำลอง Dependencies ภายนอก เช่น ฐานข้อมูลหรือ API, ซึ่งช่วยให้การทดสอบเป็นอิสระและควบคุมได้มาก...

Mocking คืออะไร: การจำลอง Dependencies ใน Unit Testing

ในบทที่แล้ว เราได้เรียนรู้เกี่ยวกับ Test Driven Development (TDD) ซึ่งเน้นการเขียน Test ก่อนเขียน Code จริง แต่บ่อยครั้งที่เราต้องทดสอบ Code ที่มีความสัมพันธ์กับส่วนประกอบอื่นๆ (Dependencies) เช่น ฐานข้อมูล, API ภายนอก หรือคลาสอื่นๆ ในโปรเจกต์ของเรา การทดสอบส่วนประกอบเหล่านี้โดยตรงอาจทำให้เกิดปัญหาหลายอย่าง เช่น

  • การพึ่งพา Environment: Test อาจล้มเหลวหากฐานข้อมูลไม่พร้อมใช้งาน หรือ API ภายนอกมีปัญหา
  • ความซับซ้อน: การตั้งค่าและจัดการ Dependencies อาจซับซ้อนและใช้เวลานาน
  • ผลข้างเคียงที่ไม่พึงประสงค์: Test อาจเปลี่ยนแปลงสถานะของ Dependencies (เช่น เขียนข้อมูลลงฐานข้อมูล) ซึ่งอาจส่งผลกระทบต่อ Test อื่นๆ

Mocking เข้ามาช่วยแก้ปัญหาเหล่านี้ โดยเป็นการจำลอง (Mock) Dependencies เหล่านั้น เพื่อให้เราสามารถทดสอบ Code ของเราได้อย่างเป็นอิสระ และควบคุมพฤติกรรมของ Dependencies ได้

ความจำเป็นของ Mocking

ลองจินตนาการว่าเรามีคลาส OrderService ที่ต้องทำงานร่วมกับคลาส PaymentGateway เพื่อประมวลผลการชำระเงิน:

class OrderService
{
    private $paymentGateway;

    public function __construct(PaymentGateway $paymentGateway)
    {
        $this->paymentGateway = $paymentGateway;
    }

    public function processOrder(Order $order)
    {
        // ... ทำอะไรบางอย่างกับ Order ...

        $paymentResult = $this->paymentGateway->charge($order->getTotalAmount());

        if ($paymentResult->isSuccessful()) {
            // ... อัปเดตสถานะ Order ...
            return true;
        } else {
            // ... จัดการข้อผิดพลาด ...
            return false;
        }
    }
}

class PaymentGateway
{
    public function charge(float $amount) : PaymentResult
    {
        // ... ติดต่อ Payment Gateway จริง ...
        // (อาจมี Network Request หรือการติดต่อฐานข้อมูล)
    }
}

class PaymentResult
{
    private $successful;

    public function __construct(bool $successful)
    {
        $this->successful = $successful;
    }

    public function isSuccessful(): bool
    {
        return $this->successful;
    }
}

ถ้าเราต้องการทดสอบ OrderService::processOrder() เราจะต้องใช้ PaymentGateway จริง ซึ่งอาจทำให้ Test ของเราไม่น่าเชื่อถือ (เนื่องจากขึ้นอยู่กับ Payment Gateway จริง) และอาจมีค่าใช้จ่าย (ถ้า Payment Gateway คิดค่าธรรมเนียม)

Mocking ช่วยให้เราสามารถสร้าง PaymentGateway ปลอม (Mock Object) ที่มีพฤติกรรมที่เราควบคุมได้ เช่น:

  • คืนค่า PaymentResult ที่สำเร็จเสมอ
  • คืนค่า PaymentResult ที่ล้มเหลวเสมอ
  • โยน Exception

ด้วยวิธีนี้ เราสามารถทดสอบ OrderService::processOrder() ได้อย่างครอบคลุม โดยไม่ต้องกังวลเรื่อง Dependencies ภายนอก

การสร้าง Mock Object

PHPUnit มีเครื่องมือที่ช่วยให้เราสร้าง Mock Object ได้อย่างง่ายดาย โดยใช้เมธอด createMock() หรือ createConfiguredMock()

ตัวอย่างการสร้าง Mock Object ด้วย createMock():

use PHPUnit\Framework\TestCase;

class OrderServiceTest extends TestCase
{
    public function testProcessOrderSuccessfulPayment()
    {
        // สร้าง Mock Object สำหรับ PaymentGateway
        $paymentGatewayMock = $this->createMock(PaymentGateway::class);

        // กำหนดพฤติกรรมของ Mock Object
        $paymentGatewayMock->method('charge')
                           ->willReturn(new PaymentResult(true));

        // สร้าง OrderService โดยใช้ Mock Object
        $orderService = new OrderService($paymentGatewayMock);

        // สร้าง Order (สมมติว่ามีคลาส Order อยู่)
        $order = new Order(100); // สมมติว่า Order มี getTotalAmount() = 100

        // เรียกใช้ method ที่เราต้องการทดสอบ
        $result = $orderService->processOrder($order);

        // ตรวจสอบผลลัพธ์
        $this->assertTrue($result);
    }
}

ในตัวอย่างนี้:

  1. เราสร้าง Mock Object สำหรับ PaymentGateway โดยใช้ $this->createMock(PaymentGateway::class)
  2. เรากำหนดพฤติกรรมของเมธอด charge() ใน Mock Object โดยใช้ $paymentGatewayMock->method('charge')->willReturn(new PaymentResult(true)) ซึ่งหมายความว่าเมื่อเรียกใช้ $paymentGatewayMock->charge() จะคืนค่า new PaymentResult(true) เสมอ
  3. เราสร้าง OrderService โดยส่ง Mock Object เข้าไป
  4. เราเรียกใช้ $orderService->processOrder() และตรวจสอบว่าผลลัพธ์เป็นไปตามที่คาดหวัง

ตัวอย่างการสร้าง Mock Object ด้วย createConfiguredMock():

use PHPUnit\Framework\TestCase;

class OrderServiceTest extends TestCase
{
    public function testProcessOrderSuccessfulPayment()
    {
        // สร้าง Mock Object และกำหนดค่า return พร้อมกัน
        $paymentGatewayMock = $this->createConfiguredMock(PaymentGateway::class, [
            'charge' => new PaymentResult(true),
        ]);

        // สร้าง OrderService โดยใช้ Mock Object
        $orderService = new OrderService($paymentGatewayMock);

        // สร้าง Order (สมมติว่ามีคลาส Order อยู่)
        $order = new Order(100); // สมมติว่า Order มี getTotalAmount() = 100

        // เรียกใช้ method ที่เราต้องการทดสอบ
        $result = $orderService->processOrder($order);

        // ตรวจสอบผลลัพธ์
        $this->assertTrue($result);
    }
}

createConfiguredMock() ช่วยให้เรากำหนดค่า return ของเมธอดหลายๆ เมธอดได้ในครั้งเดียว ทำให้ Code สั้นและอ่านง่ายขึ้น

การใช้ Mock Object ใน Test

เมื่อเรามี Mock Object แล้ว เราสามารถใช้มันเพื่อทดสอบ Code ของเราได้อย่างอิสระ โดยไม่ต้องกังวลเรื่อง Dependencies ภายนอก

ตัวอย่างการทดสอบกรณีที่ Payment ล้มเหลว:

use PHPUnit\Framework\TestCase;

class OrderServiceTest extends TestCase
{
    public function testProcessOrderFailedPayment()
    {
        // สร้าง Mock Object สำหรับ PaymentGateway และกำหนดให้ Payment ล้มเหลว
        $paymentGatewayMock = $this->createMock(PaymentGateway::class);
        $paymentGatewayMock->method('charge')
                           ->willReturn(new PaymentResult(false));

        // สร้าง OrderService โดยใช้ Mock Object
        $orderService = new OrderService($paymentGatewayMock);

        // สร้าง Order (สมมติว่ามีคลาส Order อยู่)
        $order = new Order(100); // สมมติว่า Order มี getTotalAmount() = 100

        // เรียกใช้ method ที่เราต้องการทดสอบ
        $result = $orderService->processOrder($order);

        // ตรวจสอบผลลัพธ์
        $this->assertFalse($result);
    }
}

ตัวอย่างการทดสอบกรณีที่ Payment Gateway โยน Exception:

use PHPUnit\Framework\TestCase;

class OrderServiceTest extends TestCase
{
    public function testProcessOrderPaymentGatewayThrowsException()
    {
        // สร้าง Mock Object สำหรับ PaymentGateway และกำหนดให้โยน Exception
        $paymentGatewayMock = $this->createMock(PaymentGateway::class);
        $paymentGatewayMock->method('charge')
                           ->willThrowException(new \Exception('Payment Gateway Error'));

        // สร้าง OrderService โดยใช้ Mock Object
        $orderService = new OrderService($paymentGatewayMock);

        // สร้าง Order (สมมติว่ามีคลาส Order อยู่)
        $order = new Order(100); // สมมติว่า Order มี getTotalAmount() = 100

        // คาดหวังว่า Exception จะถูกโยน
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Payment Gateway Error');

        // เรียกใช้ method ที่เราต้องการทดสอบ (ซึ่งควรจะโยน Exception)
        $orderService->processOrder($order);
    }
}

Mocking เป็นเทคนิคที่สำคัญในการทำ Unit Testing ช่วยให้เราสามารถทดสอบ Code ของเราได้อย่างเป็นอิสระ และควบคุมพฤติกรรมของ Dependencies ได้ ทำให้ Test ของเราน่าเชื่อถือและครอบคลุมมากยิ่งขึ้น

ในบทถัดไป เราจะเรียนรู้เกี่ยวกับการใช้ Data Providers เพื่อทดสอบ Code ของเราด้วยชุดข้อมูลที่หลากหลาย

Office.in.th Custom Service

Want to build or adapt a system from this structure?

Office.in.th designs and develops enterprise web applications, API integrations, and public- and private-sector management systems, with long-term support.

See our software development service