Mocking vs Stubbing vs Faking
สามคำนี้ถูกใช้สลับกันผิดบ่อยมากในวงการ ทั้งที่มีความหมายต่างกันชัดเจน — เป็นจุดที่ interviewer มักถามเพื่อเช็คความเข้าใจจริงมากกว่าแค่ท่องศัพท์
Stub — ตอบค่าคงที่ ไม่มี Logic
Stub คือ object ปลอมที่ return ค่าที่กำหนดไว้ล่วงหน้าเสมอ ไม่สนใจ input ที่รับมาเลย ไม่มี การตรวจสอบว่าถูกเรียกอย่างไร
type stubUserRepo struct{}
func (s *stubUserRepo) FindByID(id string) (*User, error) {
return &User{ID: "123", Name: "Alice"}, nil // ตอบค่าเดิมเสมอไม่ว่า id จะเป็นอะไร
}
"Stub = ป้ายคำตอบสำเร็จรูป" — เหมือนเครื่องขายของอัตโนมัติที่กดปุ่มไหนก็ได้ของชิ้นเดิมออกมา ไม่สนใจว่าใครกด ไม่เก็บสถิติการกด แค่ตอบค่าคงที่เท่านั้น
Stub ที่ "ดี" ควรออกแบบให้ ตอบตาม input ได้บ้าง (ไม่ใช่ตอบค่าเดียวตายตัวเสมอ) เพื่อให้เทส case ที่หลากหลายได้โดยไม่ต้องสร้าง stub class แยกทุกเคส — เทคนิคที่นิยมคือ "function field" stub (ดูตัวอย่างใน Go → Testing) ที่รับ closure มากำหนดพฤติกรรม ตาม test case โดยไม่เปลี่ยนนิยามว่ามันยังเป็น "stub" อยู่ (ตอบค่า ไม่ตรวจสอบ behavior) — จุดที่ แยก stub ออกจาก mock ไม่ใช่ "ตอบค่าได้แค่แบบเดียวหรือหลายแบบ" แต่คือ "ตรวจสอบว่าถูกเรียก อย่างไรหรือไม่" ต่างหาก
Mock — ตรวจสอบว่าถูกเรียกอย่างไร (Behavior Verification)
Mock ไม่ได้สนแค่ "ตอบอะไรกลับไป" แต่บันทึกว่าตัวเองถูกเรียกกี่ครั้ง ด้วย argument อะไร แล้ว ให้เทสต์ assert พฤติกรรมการเรียกนั้นได้ (ไม่ใช่แค่ assert ผลลัพธ์สุดท้าย)
mockLogger := new(MockLogger)
mockLogger.On("Error", "failed to save user").Once() // คาดหวังว่าจะถูกเรียกแบบนี้ 1 ครั้ง
svc.SaveUser(invalidUser)
mockLogger.AssertExpectations(t) // ตรวจว่าการเรียกเกิดขึ้นจริงตามที่คาดไว้
"Mock = กล้องวงจรปิดที่บันทึกพฤติกรรม" — ไม่ได้สนแค่ผลลัพธ์สุดท้ายว่าถูกไหม แต่สนใจว่า "มีการ เรียกใช้งานที่ถูกต้องเกิดขึ้นจริงไหม" (เช่น ฟังก์ชัน log error ถูกเรียกจริงไหมตอนมี error เกิดขึ้น)
Mock ที่ใช้พร่ำเพรื่อเกินไปสร้างปัญหาที่เรียกว่า "over-specified test" — เทสต์ที่ assert พฤติกรรมภายในมากเกินจำเป็น (เช่น assert ว่าฟังก์ชันภายในถูกเรียกตามลำดับเป๊ะๆ กี่ครั้ง ด้วย argument อะไรทุกตัว) ทำให้เทสต์ fragile ต่อ refactor แม้ behavior ภายนอกจะไม่เปลี่ยน เลยก็ตาม เช่น ถ้า refactor ฟังก์ชันให้เรียก logger 2 ครั้งแทนที่จะเป็น 1 ครั้ง (แต่ผลลัพธ์ สุดท้ายเหมือนเดิมทุกประการ) เทสต์ที่ mock ไว้แน่นเกินไปจะ fail ทันทีทั้งที่โค้ดไม่ได้ "พัง" จริง — หลักปฏิบัติที่ดีคือ mock เฉพาะ side effect ที่สำคัญจริงๆ ต่อ business requirement (เช่น "ต้องส่งอีเมลแจ้งเตือนจริงๆ") ไม่ใช่ mock ทุก internal call ที่เจอ เพื่อรักษาสมดุลระหว่าง ความมั่นใจกับความ fragile ของเทสต์
Fake — Implementation ง่ายๆ ที่ทำงานได้จริง
Fake คือ implementation ทางเลือกที่ ทำงานได้จริงตาม logic (ไม่ใช่แค่ตอบค่าคงที่เหมือน stub) แต่เป็นเวอร์ชันที่ "เบา" กว่าของจริง เหมาะกับการเทสต์ เช่น in-memory database แทน database server จริง
type fakeUserRepo struct {
users map[string]*User // เก็บจริงใน memory ไม่ใช่ DB จริง
}
func (f *fakeUserRepo) Save(u *User) error {
f.users[u.ID] = u // logic จริง: บันทึกแล้วดึงกลับมาได้จริง
return nil
}
func (f *fakeUserRepo) FindByID(id string) (*User, error) {
u, ok := f.users[id]
if !ok {
return nil, ErrNotFound
}
return u, nil
}
"Fake = ของเล่นจำลองที่ใช้งานได้จริง ไม่ใช่แค่หุ่นโชว์" — เหมือนรถของเล่นที่วิ่งได้จริง (มี logic จริง) แค่เล็กกว่า/เบากว่ารถจริง (ไม่ต้องต่อ network, ไม่ persist ข้าม process) ต่างจาก stub ที่เหมือนรูปรถแปะผนัง (ตอบแค่ค่าคงที่ ไม่มี logic อะไรข้างใน)
Fake มีความเสี่ยงที่คนมักไม่รู้: Fake ที่ logic ไม่ตรงกับของจริง 100% จะทำให้เทสต์ "โกหก" (ให้ false confidence) เช่น fake in-memory database ที่ไม่ได้ implement unique constraint เหมือน database จริง — เทสต์ที่ควร fail เพราะ insert ข้อมูลซ้ำ key จะผ่านฉลุยบน fake ทั้งที่ production database จริงจะ error ทันที วิธีลดความเสี่ยงนี้ (ที่ทีม mature ใช้) คือทำ "contract test" คู่กับ fake เอง — รัน test suite ชุดเดียวกันทั้งกับ fake และกับ implementation จริง (เช่น real database ใน integration test) เพื่อยืนยันว่าพฤติกรรมของ fake ตรงกับของจริงในเคสสำคัญๆ จริง ไม่ใช่แค่ "ดูเหมือนทำงานได้" เฉยๆ โดยไม่เคยเทียบกับของจริงเลย
สรุปเปรียบเทียบ
| มี Logic จริงไหม | ตรวจสอบ behavior การเรียกไหม | ตัวอย่าง | |
|---|---|---|---|
| Stub | ไม่ (ตอบค่าคงที่) | ไม่ | ฟังก์ชันที่ return ค่า hardcode |
| Mock | ไม่จำเป็น | ใช่ (จุดต่างสำคัญ) | ตรวจว่า SendEmail() ถูกเรียก 1 ครั้ง |
| Fake | ใช่ (แบบย่อ) | ไม่ (เน้นที่ผลลัพธ์ ไม่ใช่การเรียก) | In-memory DB, in-memory message queue |
Stubs return canned values regardless of input, with no behavior verification. Mocks add behavior verification — asserting how a dependency was called, not just the final result — but overusing mocks to assert internal call patterns creates "over-specified" tests that break on harmless refactors even when external behavior is unchanged; mock only side effects that matter to actual requirements. Fakes are lightweight but functionally real implementations (e.g. an in-memory database) — their risk is silently diverging from real behavior (missing constraints, etc.), giving false confidence; mature teams run the same contract tests against both the fake and the real implementation to catch drift.