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 แล้ว
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”- test เรียก factory method บน Object Mother เช่น
UserMother.Standard() - Object Mother สร้าง object ด้วยค่า default ที่ “สมเหตุสมผล” และตั้งชื่อ method ให้สื่อสถานการณ์ที่ต้องการ (
Standard,Admin,Inactive,NewlyRegisteredฯลฯ) - ถ้า test ต้องการความแตกต่างเล็กน้อยจาก fixture มาตรฐาน จะเรียก method ปรับแต่งต่อ (fluent) หรือแก้ property หลังได้ object มาแล้ว
- เมื่อ 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 แทน
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// 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 ทั้งหมดของ Userpublic 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 ช่วยลดได้