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

การใช้ Mock Objects ใน Unit Test

บทความนี้แนะนำการใช้ Mock Objects ใน Unit Test เพื่อทดสอบโค้ดโดยไม่ต้องพึ่งพา external dependencies จริง ซึ่งช่วยให้การทดสอบเป็นอิสระ รวดเร็ว และมีความแน่นอนมาก...

การใช้ Mock Objects ใน Unit Test

จากบทที่แล้ว เราได้เรียนรู้เกี่ยวกับการเขียน Unit Test เพื่อทดสอบ API ของเรา การทดสอบ Unit Test ที่ดีจะช่วยให้เรามั่นใจว่าโค้ดของเราทำงานได้อย่างถูกต้อง และสามารถเปลี่ยนแปลงโค้ดได้อย่างมั่นใจโดยไม่ต้องกลัวว่าจะทำให้ส่วนอื่น ๆ ของระบบเสียหาย ในบทนี้ เราจะมาเรียนรู้เกี่ยวกับเทคนิคการใช้ Mock Objects ซึ่งเป็นเครื่องมือสำคัญในการเขียน Unit Test ที่มีประสิทธิภาพ

Mock Objects คืออะไร?

Mock Objects คือ object จำลองที่เราสร้างขึ้นมาเพื่อเลียนแบบการทำงานของ object จริง โดยเฉพาะ object ที่มีความซับซ้อนในการสร้าง หรือมีการพึ่งพา external dependencies เช่น ฐานข้อมูล, API ภายนอก หรือระบบไฟล์

ทำไมต้องใช้ Mock Objects?

  • Isolation: ช่วยให้เราสามารถทดสอบ unit ของโค้ดได้อย่างแท้จริง โดยไม่ต้องกังวลว่า external dependencies จะส่งผลต่อผลลัพธ์ของการทดสอบ
  • Control: ช่วยให้เราสามารถควบคุมพฤติกรรมของ dependencies ได้อย่างแม่นยำ เช่น กำหนดให้ method คืนค่าที่เฉพาะเจาะจง หรือ throw exception ในสถานการณ์ที่ต้องการ
  • Speed: ช่วยให้การทดสอบรวดเร็วขึ้น เพราะไม่ต้องรอการทำงานของ dependencies จริง ซึ่งอาจใช้เวลานาน หรือมีข้อจำกัดด้านความเร็ว
  • Determinism: ช่วยให้การทดสอบมีความแน่นอน (deterministic) เพราะผลลัพธ์ของการทดสอบจะไม่ขึ้นอยู่กับสถานะของ external dependencies ที่อาจเปลี่ยนแปลงไป

การใช้งาน Mock Objects

ใน PHPUnit เราสามารถสร้าง Mock Objects ได้หลายวิธี วิธีที่ง่ายที่สุดคือการใช้ createMock() method

use PHPUnit\Framework\TestCase;

class MyClassTest extends TestCase
{
  public function testMyMethod()
  {
    // สร้าง Mock Object ของ DependencyClass
    $mockDependency = $this->createMock(DependencyClass::class);

    // กำหนดพฤติกรรมของ Mock Object
    $mockDependency->method('someMethod')
                   ->willReturn('mocked value');

    // สร้าง instance ของ MyClass โดย inject Mock Object เข้าไป
    $myClass = new MyClass($mockDependency);

    // เรียก method ที่ต้องการทดสอบ
    $result = $myClass->myMethod();

    // ทำการ assertion
    $this->assertEquals('expected value', $result);
  }
}

class MyClass {
  private $dependency;

  public function __construct(DependencyClass $dependency) {
    $this->dependency = $dependency;
  }

  public function myMethod() {
    // ... โค้ดที่ใช้ $this->dependency->someMethod() ...
    return $this->dependency->someMethod();
  }
}

class DependencyClass {
  public function someMethod() {
    // ... โค้ดที่ติดต่อกับ external dependency ...
    return 'real value';
  }
}

คำอธิบาย:

  • เราสร้าง Mock Object ของ DependencyClass โดยใช้ $this->createMock(DependencyClass::class)
  • เรากำหนดพฤติกรรมของ someMethod() ใน Mock Object โดยใช้ $mockDependency->method('someMethod')->willReturn('mocked value') ซึ่งหมายความว่า เมื่อ someMethod() ถูกเรียกบน Mock Object จะคืนค่า 'mocked value'
  • เราสร้าง instance ของ MyClass โดยส่ง Mock Object เข้าไปใน constructor
  • เมื่อ myMethod() ถูกเรียก จะใช้ someMethod() ของ Mock Object แทนที่จะเป็นของ object จริง
  • สุดท้าย เราทำการ assertion เพื่อตรวจสอบว่า myMethod() คืนค่าที่คาดหวัง

ตัวอย่าง: การ Mock ฐานข้อมูล

สมมติว่าเรามี class ที่ชื่อว่า UserRepository ซึ่งใช้ในการดึงข้อมูลผู้ใช้จากฐานข้อมูล

class UserRepository {
  private $db;

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

  public function getUserById(int $id) {
    $stmt = $this->db->prepare("SELECT * FROM users WHERE id = :id");
    $stmt->execute(['id' => $id]);
    return $stmt->fetch(PDO::FETCH_ASSOC);
  }
}

ในการทดสอบ getUserById() เราไม่ต้องการที่จะติดต่อกับฐานข้อมูลจริง เราสามารถใช้ Mock Object เพื่อจำลองการทำงานของ PDO ได้

use PHPUnit\Framework\TestCase;
use PDO;
use PDOStatement;

class UserRepositoryTest extends TestCase
{
  public function testGetUserById()
  {
    // สร้าง Mock Object สำหรับ PDOStatement
    $mockStatement = $this->createMock(PDOStatement::class);
    $mockStatement->method('execute')->willReturn(true);
    $mockStatement->method('fetch')->willReturn(['id' => 1, 'name' => 'John Doe']);

    // สร้าง Mock Object สำหรับ PDO
    $mockPDO = $this->createMock(PDO::class);
    $mockPDO->method('prepare')->willReturn($mockStatement);

    // สร้าง instance ของ UserRepository โดย inject Mock Object เข้าไป
    $userRepository = new UserRepository($mockPDO);

    // เรียก method ที่ต้องการทดสอบ
    $user = $userRepository->getUserById(1);

    // ทำการ assertion
    $this->assertEquals(['id' => 1, 'name' => 'John Doe'], $user);
  }
}

คำอธิบาย:

  • เราสร้าง Mock Object สำหรับ PDOStatement ซึ่งเป็น object ที่ใช้ในการ execute SQL query และ fetch ผลลัพธ์
  • เรากำหนดพฤติกรรมของ execute() ให้คืนค่า true และ fetch() ให้คืนค่า array ที่มีข้อมูลผู้ใช้
  • เราสร้าง Mock Object สำหรับ PDO และกำหนดพฤติกรรมของ prepare() ให้คืนค่า Mock Object ของ PDOStatement
  • เราสร้าง instance ของ UserRepository โดยส่ง Mock Object ของ PDO เข้าไป
  • เมื่อ getUserById() ถูกเรียก จะใช้ Mock Object ของ PDO และ PDOStatement แทนที่จะติดต่อกับฐานข้อมูลจริง

ประโยชน์ของการใช้ Mock Objects

  • ลดความซับซ้อน: ช่วยให้เราสามารถทดสอบโค้ดได้อย่างง่ายดาย โดยไม่ต้องกังวลเกี่ยวกับ dependencies ที่ซับซ้อน
  • เพิ่มความเร็ว: ช่วยให้การทดสอบรวดเร็วขึ้น เพราะไม่ต้องรอการทำงานของ dependencies จริง
  • เพิ่มความน่าเชื่อถือ: ช่วยให้การทดสอบมีความน่าเชื่อถือมากขึ้น เพราะผลลัพธ์ของการทดสอบจะไม่ขึ้นอยู่กับสถานะของ external dependencies
  • ทดสอบ edge cases: ช่วยให้เราสามารถทดสอบสถานการณ์ที่ผิดปกติ หรือ edge cases ได้ง่ายขึ้น เช่น กรณีที่ฐานข้อมูลไม่พร้อมใช้งาน หรือ API ภายนอกล่ม
  • ออกแบบที่ดีขึ้น: การใช้ Mock Objects บังคับให้เราออกแบบโค้ดให้มีความ loose coupling ซึ่งเป็นหลักการออกแบบที่ดี

ข้อควรระวังในการใช้ Mock Objects

  • Over-mocking: การสร้าง Mock Object มากเกินไปอาจทำให้การทดสอบซับซ้อนและยากต่อการบำรุงรักษา
  • Brittle tests: การทดสอบที่ผูกติดกับ implementation details ของ dependencies มากเกินไป อาจทำให้การทดสอบ fail เมื่อ dependencies มีการเปลี่ยนแปลง
  • False sense of security: การใช้ Mock Objects อย่างไม่ถูกต้องอาจทำให้เราเข้าใจผิดว่าโค้ดของเราทำงานได้อย่างถูกต้อง ในขณะที่จริง ๆ แล้วอาจมีข้อผิดพลาดอยู่

แนวปฏิบัติที่ดี:

  • Mock เฉพาะ dependencies ที่จำเป็นเท่านั้น
  • Mock พฤติกรรมของ dependencies ไม่ใช่ implementation details
  • เขียน integration test เพื่อทดสอบการทำงานร่วมกันของ components ต่าง ๆ

โดยสรุป Mock Objects เป็นเครื่องมือที่ทรงพลังในการเขียน Unit Test ที่มีประสิทธิภาพ ช่วยให้เราสามารถทดสอบโค้ดได้อย่างง่ายดายและน่าเชื่อถือ อย่างไรก็ตาม การใช้ Mock Objects อย่างถูกต้องเป็นสิ่งสำคัญ เพื่อหลีกเลี่ยงปัญหาที่อาจเกิดขึ้น

Office.in.th Custom Service

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

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

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