ข้าม​ไป​ยัง​เนื้อหา

Object Mother

factory ที่​คืน fixture มาตรฐาน​สำหรับ​ใช้​ใน​หลาย test

เมื่อ​ระบบ​มี​ขนาด​ใหญ่​ขึ้น test แต่ละ​ตัว​มัก​ต้อง​สร้าง object ที่​ซับซ้อน​ขึ้น​เรื่อย ๆ เพื่อ​ใช้​เป็น input — เช่น User ที่​ต้อง​มี Name, Email, Role, IsActive ครบ​ทุก field ก่อน test จะ​รัน​ได้ ถ้า​ปล่อย​ให้​แต่ละ test ประกอบ object เหล่า​นี้​ขึ้น​เอง​ซ้ำ ๆ จะ​เกิด code ซ้ำ​จำนวน​มาก (ขัด​กับ DRY) และ​เมื่อ User เพิ่ม field ใหม่ ก็​ต้อง​ไป​แก้​ทุก test ที่​สร้าง User เอง

Object Mother (ตั้ง​ชื่อ​โดย Martin Fowler) แก้​ปัญหา​นี้​ด้วย​การ​ย้าย code สร้าง object ออก​จาก​ตัว test ไป​ไว้​ใน class ตัวกลาง​ที่​ทำ​หน้าที่​เป็น “แม่” ผู้​คืน fixture มาตรฐาน​ให้ — แนวคิด​คือ​สร้าง object ตัวอย่าง​ที่ “จำ​หน้า​ได้” เช่น UserMother.Standard() หรือ UserMother.Admin() ทีม​จะ​จดจำ​ได้​ทันที​ว่าออบ​เจ​กต์ตัว​นี้​หน้าตา​เป็น​อย่างไร​โดย​ไม่​ต้อง​ไล่​อ่าน​ทุก field ช่วย​ให้ test สื่อ​ความหมาย (readable) และ​ลด​ความ​ซ้ำซ้อน​ของ setup code

Object Mother ต่อยอด​จาก Factory Method โดย​เพิ่ม method สำหรับ “ปรับ​แต่ง” (customize) การ​สร้าง object และ​บาง​ครั้ง​อัปเดต​หรือ​ลบ object ระหว่าง test ด้วย

classDiagram
    class TestA {
      +Run()
    }
    class TestB {
      +Run()
    }
    class TestC {
      +Run()
    }
    class UserMother {
      +Standard() User
      +Admin() User
      +Inactive() User
    }
    class User {
      +Name string
      +Email string
      +Role string
      +IsActive bool
    }
    TestA --> UserMother
    TestB --> UserMother
    TestC --> UserMother
    UserMother ..> User

หลาย test เรียก factory method บน class UserMother ตัว​เดียวกัน แทนที่​จะ​ประกอบ User เอง​ใน​แต่ละ test method เช่น Standard() คืน​ค่า fixture มาตรฐาน ส่วน Admin() หรือ Inactive() คืน​ค่า object แบบ​เดียวกัน​ที่​ปรับ​แต่ง​บาง field แล้ว

  1. test เรียก factory method บน Object Mother เช่น UserMother.Standard()
  2. Object Mother สร้าง object ด้วย​ค่า default ที่ “สม​เหตุ​สม​ผล” และ​ตั้ง​ชื่อ method ให้​สื่อ​สถานการณ์​ที่​ต้องการ (Standard, Admin, Inactive, NewlyRegistered ฯลฯ)
  3. ถ้า test ต้องการ​ความ​แตก​ต่าง​เล็กน้อย​จาก fixture มาตรฐาน จะ​เรียก method ปรับ​แต่ง​ต่อ (fluent) หรือ​แก้ property หลัง​ได้ object มา​แล้ว
  4. เมื่อ schema ของ User เปลี่ยน (เพิ่ม field บังคับ) จะ​แก้​แค่​จุด​เดียว​ใน class Object Mother แล้ว test ทั้งหมด​ยัง​ผ่าน​โดย​ไม่​ต้อง​แตะ code test เลย
sequenceDiagram
    participant T as Test
    participant M as UserMother
    T->>M: Standard()
    M-->>T: User fixture มาตรฐาน
    T->>M: Admin()
    M->>M: Standard() แล้วปรับ Role
    M-->>T: User ที่เป็น admin

ข้อ​ควร​ระวัง: เมื่อ​เวลา​ผ่าน​ไป class Object Mother มัก​บวม​ขึ้น​เรื่อย ๆ ด้วย method เฉพาะ​กิจ​ที่​เกือบ​ซ้ำ​กัน (เช่น AdminWithNoEmail(), AdminInactive()) จน​ละเมิด DRY เอง — เป็น​สัญญาณ​ว่า​ถึง​เวลา​พิจารณา Builder แทน

// model domain อย่างง่าย — ต้องเป็น record (C# 9+) เพื่อให้ใช้ 'with' expression ได้
public record User
{
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
public string Role { get; init; } = "member";
public bool IsActive { get; init; } = true;
// คืน object ใหม่ที่แก้ Role — ทำให้ User ไม่ mutable และ mother ปรับแต่งได้ปลอดภัย
public User WithRole(string role) => this with { Role = role };
public User Deactivate() => this with { IsActive = false };
}
// Object Mother — รวมการสร้าง fixture ของ User ไว้ที่เดียว
public static class UserMother
{
// fixture มาตรฐานที่ทุก test ใช้ร่วมกัน ตั้งชื่อให้ทีมจำได้ทันที
public static User Standard() => new()
{
Name = "ทดสอบ ผู้ใช้",
Email = "test@example.com",
Role = "member",
IsActive = true,
};
// ปรับจาก Standard() แทนที่จะประกอบใหม่ทั้งหมด — ลดจุดที่ต้องแก้เมื่อ schema เปลี่ยน
public static User Admin() => Standard().WithRole("admin");
public static User Inactive() => Standard().Deactivate();
public static User NewlyRegistered() => Standard() with
{
Name = "สมาชิกใหม่",
Email = "newbie@example.com",
};
}
// ใน test: ไม่ต้องรู้รายละเอียด field ทั้งหมดของ User
public class UserServiceTests
{
[Fact]
public void Admin_CanAccessDashboard()
{
var admin = UserMother.Admin();
var canAccess = DashboardPolicy.CanAccess(admin);
Assert.True(canAccess);
}
[Fact]
public void InactiveUser_CannotLogin()
{
var user = UserMother.Inactive();
var result = AuthService.Login(user);
Assert.False(result.Success);
}
}
  • object ที่​ต้อง​สร้าง​มี field จำนวน​มาก และ​หลาย test ต้องการ instance ที่ “หน้าตา​เหมือน​กัน” เป็น​ส่วน​ใหญ่
  • ต้องการ​ให้​ทีม​มี “คำ​ศัพท์” ร่วม​กัน​สำหรับ fixture (เช่น​พูด​ว่า “test นี้​ใช้ admin mother”) ซึ่ง​ช่วย​สื่อสาร​เจตนา test ได้​เร็ว​กว่า​ไล่​อ่าน​ทุก field
  • จำนวน variation ของ object ยัง​จำกัด ไม่​กี่​แบบ (standard, admin, inactive ฯลฯ)
  • ต้องการ​รวม​จุด​แก้ไข​ไว้​ที่​เดียว​เมื่อ constructor หรือ schema ของ object เปลี่ยน ตาม​หลัก DRY
  • ใช้​คู่​กับ Test Driven Development เพื่อ​ให้​เขียน test ใหม่​ได้​เร็ว​โดย​ไม่​ต้อง setup ซ้ำ
  • object ที่​ต้องการ​มี variation หลากหลาย​มาก — จะ​ทำให้ class Object Mother มี method เพิ่ม​ขึ้น​เรื่อย ๆ จน​กลาย​เป็น combinatorial explosion ของ​ชื่อ method (AdminInactiveNoEmail() ฯลฯ) ควร​เปลี่ยน​ไป​ใช้ Builder แบบ fluent แทน
  • test ต่าง ๆ พึ่งพา​ค่าที่​แน่นอน​ใน fixture มาตรฐาน​มาก​เกิน​ไป จน​แก้​ค่า​ใน mother ครั้ง​เดียว​แล้ว test อื่น​พัง​เป็น​ลูกโซ่ (coupling ที่ Fowler เตือน​ไว้)
  • object ที่​สร้าง​ง่าย​อยู่​แล้ว (field น้อย ไม่มี invariant ซับซ้อน) — เพิ่ม abstraction เกิน​ความ​จำเป็น ขัด​กับ YAGNI และ KISS
  • ทีม​ต้องการ property-based testing หรือ​ข้อมูล​สุ่ม​จำนวน​มาก​ที่​ไม่​ควร​ถูก​จำกัด​ด้วย​ชื่อ fixture ตายตัว
ด้านรายละเอียด
ข้อดีtest อ่าน​ง่าย​ขึ้น เพราะ​ชื่อ method สื่อ​เจตนา (Admin() แทน​การ​ไล่​ตั้ง field)
ข้อดีรวม​จุด​แก้ไข​ไว้​ที่​เดียว​เมื่อ constructor หรือ schema เปลี่ยน
ข้อดีสร้าง “ภาษา​กลาง” ของ​ทีม​สำหรับ fixture ที่​ใช้​บ่อย
ข้อ​เสียclass Object Mother มัก​บวม​ด้วย method ซ้ำ ๆ เมื่อ variation เพิ่ม​ขึ้น​ตาม​เวลา
ข้อ​เสียtest หลาย​ตัว​ผูก (couple) กับ​ค่าที่​แน่นอน​ใน mother — แก้​ค่า​เดียว​กระทบ test เป็น​วง​กว้าง
ข้อ​เสียไม่​ยืดหยุ่น​เท่า Builder เมื่อ​ต้อง​ปรับ​แต่ง​หลาย field พร้อม​กัน​แบบ ad-hoc
  • Factory Method — พื้นฐาน​ที่ Object Mother ต่อยอด​มา
  • Builder — ทาง​เลือก​ที่​ยืดหยุ่น​กว่า​เมื่อ variation ของ object มี​มาก
  • Test Driven Development — บริบท​หลัก​ที่​ใช้งาน Object Mother
  • Don’t Repeat Yourself — หลักการ​ที่ Object Mother ช่วย​รักษา​ไว้
  • YAGNI — เกณฑ์​ตัดสิน​ว่า​ควร​เริ่ม​ใช้ pattern นี้​เมื่อไหร่
  • Poor Tests — กลิ่น code ที่ Object Mother ช่วย​ลด​ได้