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);
}
}
ในตัวอย่างนี้:
- เราสร้าง Mock Object สำหรับ
PaymentGatewayโดยใช้$this->createMock(PaymentGateway::class) - เรากำหนดพฤติกรรมของเมธอด
charge()ใน Mock Object โดยใช้$paymentGatewayMock->method('charge')->willReturn(new PaymentResult(true))ซึ่งหมายความว่าเมื่อเรียกใช้$paymentGatewayMock->charge()จะคืนค่าnew PaymentResult(true)เสมอ - เราสร้าง
OrderServiceโดยส่ง Mock Object เข้าไป - เราเรียกใช้
$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 ของเราด้วยชุดข้อมูลที่หลากหลาย