Stub (สตับ, ตัวแทน) — ออบเจกต์ทดสอบที่ส่งคืนการตอบสนองที่กำหนดไว้ล่วงหน้าสำหรับการเรียกเมธอดแทนการทำงานจริง ในการพัฒนาแอปมือถือ สตับแยกโมดูลที่กำลังทดสอบออกจากคำขอเครือข่าย ฐานข้อมูล และระบบไฟล์ ทำให้สามารถตรวจสอบตรรกะได้โดยไม่ต้องตั้งค่าสภาพแวดล้อม ต่างจาก mock สตับไม่ได้ตรวจสอบพฤติกรรม — มันเพียงให้ข้อมูลเท่านั้น รายละเอียดเพิ่มเติม — ใน บทความของ Martin Fowler เกี่ยวกับ test doubles.
สาระสำคัญ
Stub — คือออบเจกต์ตัวแทนที่แทนที่การพึ่งพาจริงในการทดสอบและส่งคืนค่าที่กำหนดไว้ล่วงหน้าสำหรับการเรียกเฉพาะ คำศัพท์นี้ถูกนำเสนอในการจำแนกของ Gerard Meszaros (2007) ในหนังสือ “xUnit Test Patterns” Stub อยู่ในหมวด test doubles — ออบเจกต์ที่แทนที่คอมโพเนนต์จริงระหว่างการทดสอบ วัตถุประสงค์หลักของสตับคือการให้ข้อมูลที่คาดเดาได้แก่บล็อกที่กำลังทดสอบ โดยขจัดความไม่แน่นอนของระบบภายนอก
หลักการทำงาน — การทดสอบกำหนดค่าสตับก่อนดำเนินการ: “เมื่อเรียกเมธอด getUsers() ให้ส่งคืนรายชื่อผู้ใช้ชุดนี้” Stub ไม่มีตรรกะทางธุรกิจ ไม่ตรวจสอบลำดับการเรียก และไม่บันทึกประวัติการเข้าถึง มันเพียงยืนอยู่แทนคอมโพเนนต์จริงและส่งออกสิ่งที่ถูกสั่งให้ทำ ในบริบทของการทดสอบ Android หมายความว่า: OkHttp-client ไม่ส่งคำขอจริงไปยังเซิร์ฟเวอร์ แต่รับการตอบสนองจาก MockWebServer ที่กำหนดค่าเป็นสตับ
ควรใช้เมื่อใด — สตับเหมาะสมที่สุดสำหรับการทดสอบเลเยอร์ UI (ViewModel, Presenter) และตรรกะทางธุรกิจ (UseCase, Interactor) ซึ่งต้องตรวจสอบปฏิกิริยาต่อข้อมูลเฉพาะ: รายการว่าง, เซิร์ฟเวอร์ส่งคืนข้อผิดพลาด 500, โทเคนหมดอายุ เคสใดก็ตามที่การทดสอบต้องการสถานะอินพุตที่แน่นอน — นั่นคืองานของสตับ สำหรับแต่ละสถานการณ์ทดสอบจะสร้างการกำหนดค่าสตับของตัวเอง ทำให้การทดสอบอ่านง่ายและคาดเดาได้
Gerard Meszaros (2007) ในหนังสือ “xUnit Test Patterns” ได้ระบุ test doubles ห้าประเภท: dummy, stub, spy, mock, fake แต่ละประเภทแก้ไขงานของตัวเอง Dummy — ถูกส่งผ่านแต่ไม่ถูกใช้ Stub — ส่งคืนข้อมูล Spy — บันทึกการเรียก Mock — ตรวจสอบพฤติกรรม Fake — มีตรรกะแบบย่อ ความเข้าใจในการจำแนกนี้ช่วยให้นักพัฒนาเลือกเครื่องมือที่ถูกต้องสำหรับแต่ละสถานการณ์ทดสอบ
คำขอเครือข่าย — สถานการณ์การใช้สตับที่พบบ่อยที่สุด แอปพลิเคชันทำการเรียก HTTP ไปยัง API และในการทดสอบต้องตรวจสอบปฏิกิริยาต่อการตอบสนองต่าง ๆ: JSON ที่สำเร็จ, ข้อผิดพลาด 401 (ไม่ได้รับอนุญาต), หมดเวลา, อาร์เรย์ว่าง MockWebServer (OkHttp) บน Android และ URLProtocol (iOS) ทำหน้าที่เป็นสตับ โดยส่งคืนการตอบสนอง HTTP ที่กำหนดไว้ล่วงหน้าโดยไม่ต้องเชื่อมต่อกับเซิร์ฟเวอร์จริง ทำให้การทดสอบเร็วขึ้นจากวินาทีเป็นมิลลิวินาที
ฐานข้อมูล — Room (Android) และ CoreData (iOS) มีเวอร์ชัน in-memory แต่การตั้งค่ายังคงใช้เวลา Stub แทน repository ส่งคืนรายการ Entity ที่เตรียมไว้ล่วงหน้าโดยไม่แตะต้อง DB โดยเฉพาะอย่างยิ่งมีประสิทธิภาพสำหรับการทดสอบ ViewModel ซึ่งต้องตรวจสอบการเรียงลำดับ การกรอง หรือการแปลงข้อมูล การทดสอบทำงานภายในมิลลิวินาทีโดยไม่คำนึงถึงปริมาณข้อมูล
บริการระบบ — LocationManager, SensorManager, SharedPreferences ต้องใช้อุปกรณ์จริงหรืออีมูเลเตอร์ Stub สำหรับ LocationProvider ส่งคืนพิกัดที่กำหนด สำหรับ SensorManager — ค่าอะคเซเลอโรมิเตอร์คงที่ บน iOS สิ่งที่เทียบเท่าคือ CLLocationManager พร้อม implementation ของ delegate สำหรับทดสอบ หากไม่มีสตับ การทดสอบเหล่านี้ต้องใช้อุปกรณ์กายภาพที่มีเงื่อนไขเฉพาะ
ระบบไฟล์และแคช — การโหลดรูปภาพ การแคชการตอบสนอง การทำงานกับไฟล์การกำหนดค่า — การดำเนินการทั้งหมดนี้ขึ้นอยู่กับสถานะของดิสก์ Stub สำหรับ FileManager หรือ ImageCache ส่งคืนความสำเร็จ/ข้อผิดพลาดโดยไม่ต้องอ่านไฟล์จริง ซึ่งช่วยกำจัดการพังปลอมของการทดสอบเนื่องจากเส้นทางหรือสิทธิ์การเข้าถึงไม่ตรงกันบนเครื่องของนักพัฒนาที่ต่างกัน
การแบ่งความรับผิดชอบ — test doubles สามประเภทแก้ไขงานที่แตกต่างกัน Stub: “ให้ข้อมูลฉัน”. Mock: “ตรวจสอบว่าฉันถูกเรียก”. Fake: “ฉันทำงานเหมือนของจริงเพียงง่ายกว่า” ความแตกต่างสำคัญอย่างยิ่งต่อความอ่านง่ายของการทดสอบ: หากการทดสอบใช้ mock ในที่ที่ต้องการ stub การทดสอบจะถูกถ่วงด้วยการเรียก verify ที่ไม่เกี่ยวข้องกับสถานการณ์ที่ตรวจสอบ
| คุณลักษณะ | Stub | Mock | Fake |
|---|---|---|---|
| วัตถุประสงค์ | ให้ข้อมูล | ตรวจสอบปฏิสัมพันธ์ | implementation แบบย่อ |
| ตรรกะ | ไม่มี | ไม่มี | มี (แต่แบบย่อ) |
| การตรวจสอบ | ไม่มี | มี (verify) | ทางอ้อม (ผ่านสถานะ) |
| ความยืดหยุ่น | ต่ำ — การตอบสนองตายตัว | ปานกลาง | สูง — ตรรกะปรับตัวได้ |
| ความเร็ว | สูงสุด | สูง | ปานกลาง |
| ตัวอย่าง | MockWebServer ส่งคืน JSON | Mockito.verify(repository).save() | InMemoryRepository ด้วย HashMap |
กฎเชิงปฏิบัติ — หากการทดสอบตรวจสอบว่าคอมโพเนนต์ที่กำลังทดสอบได้รับข้อมูลใด — ใช้ stub หากการทดสอบตรวจสอบว่าคอมโพเนนต์เรียกเมธอดของการพึ่งพาด้วยอาร์กิวเมนต์ที่ถูกต้องหรือไม่ — ใช้ mock หากคุณเพียงต้องการแทนที่ DB ด้วยตารางแฮช — นั่นคือ fake การผสมประเภทในการทดสอบเดียวทำให้การทดสอบเปราะ: เมื่อเปลี่ยน implementation จะต้องเขียนทั้ง stub และตรรกะ verify ใหม่
Stub กับ verify — ข้อผิดพลาดที่พบบ่อยเมื่อนักพัฒนากำหนดค่า stub แล้วเพิ่ม verify(stub).method() ตามนิยาม สตับไม่ควรถูกตรวจสอบ — สำหรับการตรวจสอบมี mock หากคุณต้องตรวจสอบว่าเมธอดถูกเรียกด้วยอาร์กิวเมนต์เฉพาะ ใช้ Mockito.mock() แทน Mockito.stub() การแยกนี้ทำให้เจตนาของการทดสอบชัดเจนสำหรับนักพัฒนาคนอื่น
MockWebServer — ไลบรารี OkHttp สำหรับสร้าง HTTP-สตับบน Android และ JVM มันรันเซิร์ฟเวอร์ HTTP ภายในบนพอร์ตที่ระบุ ซึ่งสกัดกั้นคำขอของ OkHttp-client และส่งคืนการตอบสนองที่กำหนดไว้ล่วงหน้า การตั้งค่าใช้เพียงสามบรรทัด: สร้างเซิร์ฟเวอร์, enqueue การตอบสนอง, เริ่มรัน การทดสอบสามารถ enqueue หลายการตอบสนองตามลำดับสำหรับสถานการณ์ที่มี pagination หรือ retry
class UserRepositoryTest {
private val server = MockWebServer()
fun setup() {
server.start(8080)
val client = OkHttpClient.Builder()
.readTimeout(1, TimeUnit.SECONDS)
.build()
}
fun test_user_list_success() {
val json = "[{\"id\":1,\"name\":\"Alice\"}]"
server.enqueue(MockResponse()
.setBody(json)
.setResponseCode(200)
)
val result = repository.getUsers()
assertEquals(1, result.size)
}
fun teardown() {
server.shutdown()
}
}
MockK — ทางเลือกแทน Mockito สำหรับ Kotlin ที่รองรับ coroutine, extension-function และ sealed-class แบบ first-class สตับใน MockK สร้างผ่าน coEvery (สำหรับ suspend-function) และ every (สำหรับฟังก์ชันปกติ) ต่างจาก MockWebServer MockK สร้างสตับสำหรับเมธอดการพึ่งพาแต่ละเมธอด ไม่ใช่ทั้งเลเยอร์ HTTP สะดวกสำหรับ unit-test ของ UseCase หรือ Interactor ซึ่งการพึ่งพาคือ abstraction ของ repository
interface UserRepository {
suspend fun getUsers(): List<User>
}
class GetUsersUseCaseTest {
private val repo = mockk<UserRepository>()
private val useCase = GetUsersUseCase(repo)
fun test_empty_list() = runTest {
coEvery { repo.getUsers() } returns emptyList()
val result = useCase.invoke()
assertTrue(result.isEmpty())
coVerify(exactly = 1) { repo.getUsers() }
}
}
Best practice — สำหรับ integration test ใช้ MockWebServer (สกัดกั้น HTTP จริง) สำหรับ unit-test — MockK (สร้างสตับสำหรับ interface) อย่าสร้างสตับสำหรับสิ่งที่คุณไม่ได้ทดสอบ: หากการทดสอบตรวจสอบ Repository อย่าสร้างสตับสำหรับ OkHttp-client ข้างใน — ใช้ MockWebServer จริงในระดับ HTTP กฎนี้ทำให้การทดสอบเกี่ยวข้องและลดความเปราะเมื่อ refactoring
โปรโตคอล Swift เป็นสตับ — ในแนวทาง iOS-native สตับถูกสร้างโดยการแทนที่โครงสร้างสำหรับทดสอบที่สอดคล้องกับโปรโตคอลของการพึ่งพา แทน NetworkService จริง การทดสอบได้รับ StubNetworkService ซึ่งส่งคืนข้อมูลคงที่ Swift เป็นภาษาที่มีชนิดแบบสถิต ดังนั้นสตับต้องสอดคล้องกับโปรโตคอลเดียวกับบริการจริง คอมไพเลอร์รับประกันว่าสตับ implement เมธอดที่จำเป็นทั้งหมด
protocol NetworkServiceProtocol {
func fetchUsers() async throws -> [User]
}
struct StubNetworkService: NetworkServiceProtocol {
let result: Result<[User], Error>
func fetchUsers() async throws -> [User] {
try result.get()
}
}
final class UsersViewModelTests: XCTestCase {
func test_success_state() async {
let stub = StubNetworkService(
result: .success([User(name: "Alice")])
)
let vm = UsersViewModel(service: stub)
await vm.load()
XCTAssertEqual(vm.users.count, 1)
}
}
OCMock สำหรับ Objective-C — ไลบรารีสำหรับสร้างสตับและม็อกในโปรเจกต์ iOS legacy OCMock รองรับ stub-method ที่มีอาร์กิวเมนต์และค่าส่งคืน โปรเจกต์ Swift สมัยใหม่นิยมแนวทางที่ใช้โปรโตคอลกับสตับที่เขียนเอง — ให้การควบคุมทุกเมธอดและไม่ต้องมีการพึ่งพาภายนอก OCMock ยังคงเป็นทางเลือกสำหรับโปรเจกต์ที่การทำโปรโตคอลทุกการพึ่งพาไม่คุ้มค่า
URLProtocol สำหรับ HTTP-สตับ — กลไกระบบของ iOS สำหรับสกัดกั้นคำขอเครือข่ายผ่านคลาสย่อยของ URLProtocol การทดสอบลงทะเบียน URLProtocol ที่กำหนดเอง ซึ่งสกัดกั้น URLSession และส่งคืน stub-response ข้อดีเหนือสตับที่เขียนเอง: ไม่ต้องเปลี่ยนสถาปัตยกรรมของแอป — URLSession ยังคงจริง แต่ข้อมูลถูกแทนที่ในระดับโปรโตคอล ข้อเสีย: ดีบักยากกว่าบริการสตับที่ชัดเจน
คำถามที่พบบ่อย
Stub ส่งคืนข้อมูลที่กำหนดไว้ล่วงหน้าและไม่ตรวจสอบข้อเท็จจริงของการเรียก Mock เพิ่มการตรวจสอบว่าเมธอดถูกเรียกด้วยอาร์กิวเมนต์ที่ถูกต้อง (verify) Stub ตอบคำถาม “ส่งคืนอะไร”, Mock — ตอบคำถาม “มีการเรียกหรือไม่” ใช้สตับเพื่อตรวจสอบสถานะ ใช้ mock เพื่อตรวจสอบปฏิสัมพันธ์
Fake จำเป็นเมื่อการทดสอบต้องการ implementation ที่ทำงานได้ (แม้แบบย่อ) — เช่น ฐานข้อมูล in-memory แทน Room Stub เหมาะสำหรับสถานการณ์เดี่ยวที่มีข้อมูลที่กำหนดไว้ล่วงหน้า หากคุณทำซ้ำสตับเดิมใน 10 การทดสอบ — มีแนวโน้มว่าคุณต้องการ Fake Fake ลดการซ้ำซ้อนเพราะตรรกะอยู่ในคลาสเดียว
บน Android — MockK สำหรับออบเจกต์ Kotlin (object) รองรับ mockkObject() รวมถึงเมธอด static ของคลาส Java ผ่าน mockkStatic() บน iOS — เมธอด static ของ Swift ไม่สามารถสตับได้โดยตรง; ใช้โปรโตคอลและ DI เพื่อแทนที่การเรียก static ด้วยเมธอด instance ของโปรโตคอล สตับแบบ static คือหนี้ทางเทคนิค ควรหลีกเลี่ยงในโค้ดใหม่
ใช้ MockWebServer (OkHttp) — มันทำงานเหมือนเซิร์ฟเวอร์ HTTP ภายในที่ enqueue การตอบสนอง สำหรับ Retrofit เพียงเปลี่ยน base URL เป็น localhost:8080 สำหรับ Ktor ใช้ MockEngine — กลไกในตัวสำหรับแทนที่ HttpStatement ทั้งสองวิธีทำงานโดยไม่ต้องใช้อินเทอร์เน็ตจริงและให้การควบคุมรหัสสถานะ เนื้อหา และส่วนหัวของการตอบสนองอย่างเต็มที่
Spy ห่อหุ้มออบเจกต์จริงและบันทึกการเรียก ในขณะที่ Stub แทนที่ออบเจกต์ทั้งหมดด้วยการตอบสนองคงที่ Spy อนุญาตให้ใช้ implementation จริงบางส่วน (เมธอดอื่นทำงานตามเดิม) แต่สตับไม่ หากคุณต้องตรวจสอบว่าเมธอดถูกเรียก แต่บางส่วนของตรรกะควรทำงาน — ใช้ spy ไม่ใช่ stub
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม