Fake คือการใช้งานที่เรียบง่ายและทำงานได้ของการพึ่งพาที่ทำงานเหมือนส่วนประกอบจริง แต่ใช้พื้นที่จัดเก็บในหน่วยความจำหรือกลไกน้ำหนักเบาอื่นๆ แทนโครงสร้างพื้นฐานการผลิต แตกต่างจาก stub ตรงที่ fake มีตรรกะทางธุรกิจจริง — การเรียงลำดับ การกรอง การรวม — เพียงแต่ไม่มีผลกระทบภายนอก ฐานข้อมูลในหน่วยความจำแทน Room หรือ HashMap แทน SharedPreferences เป็นตัวอย่างคลาสสิก รายละเอียดเพิ่มเติมใน การจำแนกประเภท test doubles ของ Martin Fowler
ประเด็นสำคัญ
Fake คือการใช้งานที่สมบูรณ์แต่น้ำหนักเบาของอินเทอร์เฟซ เหมาะสำหรับการทดสอบ คำศัพท์นี้ถูกนำเสนอโดย Gerard Meszaros (2007) ในหนังสือ “xUnit Test Patterns” แตกต่างจาก stub ที่ส่งคืนคำตอบที่ถูกกำหนดไว้ล่วงหน้า fake มีโค้ดที่สามารถทำงานได้: สามารถเรียงลำดับรายการ กรองตามเงื่อนไข นับจำนวนเรคคอร์ด ข้อแตกต่างเพียงอย่างเดียวจากการใช้งานในการผลิตคือ fake ทำงานกับข้อมูลในหน่วยความจำและไม่ดำเนินการ I/O จริง
ข้อได้เปรียบหลัก คือความเร็ว การทดสอบด้วย fake ทำงานในหน่วยมิลลิวินาทีเนื่องจากไม่มีการเข้าถึงดิสก์ เครือข่าย หรือฐานข้อมูล HashMap ในหน่วยความจำทำงานเร็วกว่า Room หรือ CoreData 100–1000 เท่า ในขณะเดียวกัน fake ก็ทดสอบตรรกะทางธุรกิจจริง: การเรียงลำดับ การกรอง การรวม — ทุกสิ่งที่ stub ไม่สามารถทดสอบได้เพราะ stub ส่งคืนเฉพาะสิ่งที่บอกเท่านั้น Fake ให้ความมั่นใจว่าโค้ดประมวลผลข้อมูลอย่างถูกต้อง แทนที่จะรับคำตอบที่กำหนดไว้ล่วงหน้า
Fake ดีกว่า Stub — หากส่วนประกอบที่กำลังทดสอบดำเนินการหลายอย่างกับข้อมูล (เรียกค้น กรอง เรียงลำดับ บันทึก) stub จะต้องกำหนดค่าการเรียกแต่ละครั้งแยกกัน Fake มีตรรกะอยู่ภายใน — การทดสอบเพียงเรียกเมธอดและตรวจสอบผลลัพธ์ ที่ IT Sectr เราใช้ fakes สำหรับที่เก็บทั้งหมดในการทดสอบหน่วย: ที่เก็บ fake ด้วย HashMap ครอบคลุม 90% ของสถานการณ์โดยไม่ต้องกำหนดค่า Mockito หรือ MockK
เกณฑ์การเลือก — กำหนดว่าการทดสอบตรวจสอบอะไร: สถานะหรือการโต้ตอบ หากการทดสอบตรวจสอบสถานะ (ผลลัพธ์ของการทำงาน) และใช้ตรรกะ — ใช้ fake หากการทดสอบต้องการเพียงข้อมูลนำเข้าโดยไม่มีตรรกะ — stub ก็เพียงพอ หากการทดสอบตรวจสอบว่ามีการเรียกเมธอด — ใช้ mock การผสมประเภท test doubles ในการทดสอบเดียวทำให้เข้าใจยากและเพิ่มความเปราะบาง
| เกณฑ์ | Fake | Stub | Mock |
|---|---|---|---|
| มีตรรกะ | ใช่ (เรียบง่าย) | ไม่ | ไม่ |
| ความเร็ว | สูง | สูงสุด | สูง |
| การตรวจสอบพฤติกรรม | ทางอ้อม | ไม่ | ใช่ (verify) |
| การบำรุงรักษา | หนึ่งคลาสต่ออินเทอร์เฟซ | กำหนดค่าต่อการทดสอบ | กำหนดค่าต่อการทดสอบ |
| ความสมจริง | สูง (โค้ดทำงาน) | ต่ำ (ข้อมูลคงที่) | ปานกลาง |
| ความเสี่ยงผลบวกลวง | ต่ำ | ปานกลาง | สูง (การทดสอบเปราะบาง) |
รูปแบบต้าน: Fake ที่ไม่ใช่ fake — ข้อผิดพลาดทั่วไปเมื่อนักพัฒนาเรียกวัตถุว่า fake ซึ่งจริงๆ แล้วเป็น stub หรือ mock ถ้า InMemoryUserRepository ของคุณไม่มีตรรกะ (การกรอง การเรียงลำดับ) — มันไม่ใช่ fake แต่เป็น stub ที่มีพื้นที่จัดเก็บในหน่วยความจำ Fake แตกต่างจาก stub ตรงที่มีตรรกะที่สามารถทำงานได้ ถ้าที่เก็บ fake เพียงส่งคืนสิ่งที่ถูกใส่เข้าไปและไม่ประมวลผลข้อมูล — ใช้ mock หรือ stub
คำแนะนำเชิงปฏิบัติ — เริ่มต้นด้วย fake สำหรับทุกที่เก็บหรือบริการ ถ้า fake ซับซ้อนเกิน 50 บรรทัด — แบ่งเป็นหลายคลาส ถ้าไม่ต้องการ fake เลย (การทดสอบตรวจสอบเพียงสถานการณ์เดียวด้วยข้อมูลคงที่) — ใช้ stub ถ้าการทดสอบตรวจสอบว่าเมธอดถูกเรียกด้วยพารามิเตอร์เฉพาะ — ใช้ mock อย่าเพิ่มประสิทธิภาพการเลือกไว้ล่วงหน้า: เขียน fake และถ้ามันมากเกินไป ให้แทนที่ด้วย stub ในการทดสอบเฉพาะ
ที่เก็บ fake สำหรับ Room — ตัวอย่างทั่วไปของ fake บน Android การใช้งานการผลิตของ UserRepository ใช้ Room DAO กับคำสั่ง SQLite เวอร์ชัน fake เก็บข้อมูลใน MutableList หรือ HashMap และใช้เมธอดเดียวกัน: getUser(id), saveUser(user), deleteUser(id) fake มีตรรกะการค้นหา การกรอง และการเรียงลำดับ — เหมือนกับที่เก็บในการผลิต แต่ไม่มี SQL ซึ่งช่วยให้ทดสอบ ViewModel และ UseCase โดยไม่ต้องตั้งค่าฐานข้อมูล Room
class FakeUserRepository : UserRepository {
private val users = mutableListOf<User>()
override suspend fun getUser(id: String): User? {
return users.find { it.id == id }
}
override suspend fun saveUser(user: User) {
val index = users.indexOfFirst { it.id == user.id }
if (index >= 0) users[index] = user
else users.add(user)
}
override suspend fun search(query: String): List<User> {
return users.filter {
it.name.contains(query, ignoreCase = true)
}
}
}
Fake สำหรับ Retrofit API — แทน MockWebServer (ซึ่งเป็น stub ไม่ใช่ fake) คุณสามารถสร้างการใช้งาน ApiService ที่ส่งคืนข้อมูลจากคอลเล็กชันในหน่วยความจำ ข้อแตกต่าง: MockWebServer สกัดกั้น HTTP และส่งคืน JSON ในขณะที่ fake ApiService ทำงานที่ระดับอินเทอร์เฟซ Kotlin โดยไม่ต้องซีเรียลไลซ์ Fake เร็วกว่า (ไม่มีการแยกวิเคราะห์ JSON) และแก้ไขจุดบกพร่องง่ายกว่า (ทำงานในโพรเซสเดียวกัน มีชนิด) เหมาะสำหรับการทดสอบที่ความหมาย HTTP (รหัสสถานะ ส่วนหัว) ไม่สำคัญ
Fake ใน Swift — สร้างผ่านโพรโทคอล คลาสการผลิตใช้โพรโทคอลด้วยตรรกะจริง (CoreData, URLSession) โครงสร้าง fake ใช้โพรโทคอลเดียวกันด้วยพื้นที่จัดเก็บในหน่วยความจำและตรรกะที่เรียบง่าย Swift เป็นภาษาที่มีความหมายเชิงค่า ดังนั้นโครงสร้าง fake จึงไม่เปลี่ยนแปลงและปลอดภัยในการทดสอบแบบหลายเธรด ซึ่งให้ข้อได้เปรียบเหนือสิ่งที่เทียบเท่า Android: ไม่จำเป็นต้องซิงโครไนซ์การเข้าถึงข้อมูลในหน่วยความจำ
protocol UserRepositoryProtocol {
func getUser(id: String) async -> User?
func saveUser(user: User) async
}
final class FakeUserRepository: UserRepositoryProtocol {
private var storage: [String: User] = [:]
func getUser(id: String) async -> User? {
return storage[id]
}
func saveUser(user: User) async {
storage[user.id] = user
}
}
final class UserViewModelTests: XCTestCase {
func test_save_and_load() async {
let fake = FakeUserRepository()
let vm = UserViewModel(repository: fake)
let user = User(id: "1", name: "Alice")
await vm.saveUser(user)
let loaded = await vm.getUser(id: "1")
XCTAssertEqual(loaded?.name, "Alice")
}
}
Fake สำหรับ CoreData — ในโปรเจกต์ iOS คุณสามารถสร้าง NSPersistentContainer ในหน่วยความจำโดยตั้งค่า description.type = NSInMemoryStoreType นี่คือสแต็ก CoreData ที่สมบูรณ์ แต่ทำงานในหน่วยความจำ fake ดังกล่าวช่วยให้ทดสอบ NSFetchRequest ภาคแสดง และการเรียงลำดับโดยไม่ต้องสร้างไฟล์ SQLite ความเร็ว: การทดสอบบน CoreData ในหน่วยความจำทำงานเร็วกว่าบนดิสก์ 5–10 เท่า ข้อเสีย: ต้องตั้งค่า NSManagedObjectModel ทุกครั้ง
FakeURLProtocol — คลาสย่อยของ URLProtocol สำหรับสกัดกั้นคำขอเครือข่ายบน iOS ลงทะเบียนผ่าน URLProtocol.registerClass(fakeProtocol) ภายในประกอบด้วยพจนานุกรม URL -> Data ในหน่วยความจำและส่งคืนข้อมูลโดยไม่มีคำขอจริง ข้อแตกต่างจาก stub: FakeURLProtocol สามารถตรวจสอบเนื้อหาคำขอ ส่วนหัว และส่งคืนการตอบสนองที่แตกต่างกันตามข้อมูลนำเข้า นี่คือ fake เพราะมีตรรกะการกำหนดเส้นทางคำขอ
Fake เป็น Test Fixture — วางคลาส fake ในโมดูลการทดสอบที่ใช้ร่วมกัน (androidTest/sharedTest หรือ TestSupport) การทดสอบทั้งหมดในโปรเจกต์ใช้ InMemoryUserRepository เดียวกัน ซึ่งช่วยลดความซ้ำซ้อนของการตั้งค่าวัตถุ mock ในการทดสอบแต่ละครั้งและรับประกันพฤติกรรมที่สม่ำเสมอ การเปลี่ยนตรรกะของ fake จะอัปเดตการทดสอบทั้งหมดพร้อมกัน ที่ IT Sectr เราเก็บคลาส fake ไว้ใน sharedTest/java/com/itSectr/fake/ และรวมผ่าน implementation project(:sharedTest)
Fake พร้อมข้อมูลที่กำหนดไว้ล่วงหน้า — การทดสอบมักต้องการที่เก็บที่มีเรคคอร์ดอยู่แล้ว วิธีแก้: เมธอดโรงงาน fakeWithData(vararg items) หรือเมธอดในตัว addDefaultData() โรงงานสร้าง fake เติมด้วยข้อมูลทั่วไปและส่งคืนวัตถุที่พร้อมใช้งาน ซึ่งลดโค้ดซ้ำซ้อนในการทดสอบ: แทนที่จะตั้งค่าการเรียก mock การทดสอบเพียงเรียก FakeUserRepository.withUsers(alice, bob)
Fake พร้อมการนับการเรียก — บางครั้งจำเป็นต้องตรวจสอบไม่เพียงแค่สถานะ แต่ยังรวมถึงจำนวนครั้งที่เรียกด้วย fake สามารถมีตัวนับ: saveCallCount, getUserCallCount การทดสอบตรวจสอบตัวนับหลังการทำงาน นี่คือการประนีประนอมระหว่าง fake บริสุทธิ์ (การตรวจสอบสถานะ) และ mock (การตรวจสอบการโต้ตอบ) ตัวนับไม่ตรวจสอบอาร์กิวเมนต์หรือลำดับการเรียก — เฉพาะจำนวน สำหรับการตรวจสอบอาร์กิวเมนต์ ให้ใช้ mock
Fake พร้อม Callback — สำหรับการทดสอบสถานการณ์อะซิงโครนัส fake สามารถรับ callback ในการเรียกแต่ละครั้ง: beforeGetUser, afterSaveUser ซึ่งช่วยให้จำลองความล่าช้า ข้อผิดพลาด หรือตรวจสอบสถานะกลาง วิธีการนี้มีประโยชน์สำหรับการทดสอบสถานะการโหลดของ UI: fake หยุดชั่วคราว 100 มิลลิวินาที และการทดสอบตรวจสอบว่าหน้าจอแสดงตัวโหลด Callback ไม่มีในการผลิต — นี่คือฟังก์ชันการทดสอบเท่านั้น
คำถามที่พบบ่อย
Fake มีตรรกะที่ทำงานได้ — กรอง เรียงลำดับ นับ Stub ส่งคืนเฉพาะคำตอบที่กำหนดไว้ล่วงหน้าโดยไม่มีตรรกะ ถ้าวัตถุมีการแตกกิ่ง (if/else, when) — มันคือ fake ถ้ามันมีเฉพาะค่าส่งคืน — มันคือ stub Fake มีค่าใช้จ่ายในการบำรุงรักษามากกว่าแต่ให้การทดสอบที่สมจริงกว่า
เมื่อตรรกะของ fake ไม่ตรงกับตรรกะการผลิต ตัวอย่างเช่น FakeUserRepository ใช้การค้นหาที่คำนึงถึงตัวพิมพ์ใหญ่เล็ก ในขณะที่เวอร์ชันการผลิตไม่คำนึงถึงตัวพิมพ์ใหญ่เล็ก การทดสอบผ่าน แต่ในความเป็นจริงมีบั๊ก วิธีแก้: ทดสอบตรรกะของ fake แยกต่างหากหรือใช้ fakes สำหรับอินเทอร์เฟซที่มีตรรกะง่ายเท่านั้น (การดำเนินการ CRUD) สำหรับตรรกะที่ซับซ้อน ให้เขียนการทดสอบการรวมกับฐานข้อมูลจริง
ฐานข้อมูลในหน่วยความจำ เป็น fake ประเภทหนึ่ง Room.inMemoryDatabaseBuilder() สร้าง SQLite ในหน่วยความจำที่ทำงานเหมือนฐานข้อมูลการผลิต นี่คือ fake ที่สมบูรณ์ แต่ fake สามารถอยู่ที่ระดับที่เก็บ (ไม่มี SQL) และระดับเครือข่าย (FakeApiService) ฐานข้อมูลในหน่วยความจำเป็นกรณีพิเศษของ fake ที่ตรรกะใกล้เคียงกับความเป็นจริงมากที่สุด
ได้ แต่ด้วยความระมัดระวัง Fake สำหรับที่เก็บ (ข้อมูล), Mock สำหรับ AnalyticsTracker (การตรวจสอบเหตุการณ์) การแบ่งตามชั้น: fake สำหรับชั้นข้อมูล, mock สำหรับชั้นการวิเคราะห์/การบันทึก อย่าทำให้วัตถุเดียวเป็นทั้ง fake และ mock ในเวลาเดียวกัน — นี่ละเมิดหลักการความรับผิดชอบเดียวและทำให้การทดสอบสับสน
ทดสอบ fake ด้วยการทดสอบเดียวกับการใช้งานการผลิต ถ้าคุณมี UserRepositoryTest ที่ตรวจสอบ save, get, delete — เรียกใช้สองครั้ง: ด้วย FakeUserRepository และด้วย RealUserRepository ซึ่งรับประกันว่า fake ทำซ้ำพฤติกรรมของคลาสการผลิต ถ้า fake เริ่มทำงานแตกต่าง — การทดสอบจะล้มเหลวในการใช้งานทั้งสอง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ