Skip to main content

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 = ป้ายคำตอบสำเร็จรูป" — เหมือนเครื่องขายของอัตโนมัติที่กดปุ่มไหนก็ได้ของชิ้นเดิมออกมา ไม่สนใจว่าใครกด ไม่เก็บสถิติการกด แค่ตอบค่าคงที่เท่านั้น

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

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 เกิดขึ้น)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

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 อะไรข้างใน)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

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
English Summary

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.