ข้ามไปยังเนื้อหาหลัก
คู่มือระบบ เนื้อหาผ่านการตรวจสอบแล้ว

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

ต้องการพัฒนาระบบหรือปรับแต่งจากโครงสร้างนี้?

Office.in.th รับพัฒนาและออกแบบ Enterprise Web Application, API Integration และระบบบริหารงานภาครัฐ/เอกชน พร้อมการสนับสนุนระยะยาว

ดูบริการรับพัฒนาซอฟต์แวร์