Test-Driven Development (TDD) คือระเบียบวิธีการพัฒนาที่เขียนเทสก่อนการimplement โค้ด นักพัฒนาจะกำหนดพฤติกรรมที่คาดหวังในรูปแบบของเทสที่ล้มเหลวก่อน จากนั้นเขียนโค้ดขั้นต่ำเพื่อให้ผ่านเทส และหลังจากนั้นปรับโครงสร้างผลลัพธ์ ตามที่ Martin Fowler (2023) กล่าว TDD ไม่ใช่เทคนิคการทดสอบ — มันเป็นเทคนิคการออกแบบที่ทำให้สถาปัตยกรรมมีวินัยและลดจำนวนข้อบกพร่องในขั้นตอนการเขียนโค้ด
ประเด็นสำคัญ
Test-Driven Development คือแนวปฏิบัติการพัฒนาซอฟต์แวร์ที่เทสอัตโนมัติกำหนดการเขียนโค้ดโปรดักชัน ต่างจากแนวทางดั้งเดิมที่เขียนโค้ดแล้วทดสอบ TDD กลับลำดับ: เขียนเทสก่อน จากนั้นเขียนโค้ดที่ผ่านเทสนั้น
ผู้ก่อตั้ง TDD คือ Kent Beck ซึ่งกำหนดแนวปฏิบัตินี้ขึ้นในช่วงปลายทศวรรษ 1990 ซึ่งเป็นส่วนหนึ่งของระเบียบวิธี Extreme Programming (XP) ในหนังสือ “Test-Driven Development: By Example” (2002) เบคได้อธิบายกฎห้าข้อของ TDD ที่กลายเป็นมาตรฐาน: เขียนเทสก่อนโค้ดโปรดักชัน เขียนโค้ดเท่าที่จำเป็นเพื่อให้ผ่านเทส และปรับโครงสร้างหลังจากแต่ละรอบ
หลักการแรก — เทสกำหนดอินเทอร์เฟซ นักพัฒนาถูกบังคับให้คิดว่าคอมโพเนนต์จะถูกใช้อย่างไรก่อนที่จะคิดว่ามันถูกimplement อย่างไร สิ่งนี้สร้าง API ที่สะอาดตั้งแต่เริ่มต้น
หลักการที่สอง — การimplement ขั้นต่ำ เมื่อเขียนเทสแล้ว นักพัฒนาจะเขียนโค้ดโปรดักชันเท่าที่จำเป็นเพื่อให้ผ่านเทส — ไม่เกินแม้แต่บรรทัดเดียว สิ่งนี้ป้องกันการสร้างนามธรรมก่อนเวลาอันควรและความซับซ้อนที่มากเกินไป ซึ่ง Martin Fowler เรียกว่า Speculative Generality
ความแตกต่างหลักระหว่าง TDD กับการทดสอบ “ทีหลัง” — วินัยของลำดับ ใน TDD เทสไม่เพียงตรวจสอบโค้ด — แต่มันนำทางโครงสร้างของโค้ด ตามการศึกษาของ Microsoft Research (Nagappan et al., 2008) ทีมที่ใช้ TDD แสดงความหนาแน่นของข้อบกพร่องลดลง 40–90% เมื่อเทียบกับทีมที่ใช้แนวทางดั้งเดิม
วงจร Red-Green-Refactor คือลำดับสามขั้นตอนที่ทำซ้ำสำหรับแต่ละเทสใหม่ Red: เขียนเทสที่ไม่ผ่าน Green: เขียนโค้ดขั้นต่ำเพื่อให้เทสผ่าน Refactor: ปรับปรุงโค้ดโดยไม่เปลี่ยนพฤติกรรม
นักพัฒนาเขียนเทสที่ตรวจสอบฟังก์ชันการทำงานที่ยังไม่ได้implement ในขั้นตอนนี้ เทสต้อง ล้มเหลว — สิ่งนี้ยืนยันว่าเทสกำลังตรวจสอบบางสิ่งจริงๆ ในสภาพแวดล้อมการพัฒนา Android เฟรมเวิร์ก JUnit 5 จะแสดงตัวบ่งชี้สีแดงสำหรับเทสที่ล้มเหลว ซึ่งเป็นที่มาของชื่อขั้นตอนนี้
class CalculatorTest {
fun testAddition() {
val result = Calculator().add(2, 3)
Assertions.assertEquals(5, result)
}
}
ในขั้นตอนนี้ จะเขียนโค้ดโปรดักชันขั้นต่ำที่เพียงพอสำหรับการผ่านเทส ไม่มีความซ้ำซ้อน — เฉพาะสิ่งที่จำเป็นสำหรับตัวบ่งชี้สีเขียว หากการimplement สามารถเป็นค่าคงที่ ก็ให้เป็นค่าคงที่ การปรับโครงสร้างจะเกิดขึ้นในขั้นตอนถัดไปเมื่อมีเทสใหม่ปรากฏขึ้น
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
}
เทสสีเขียวคือประกันสำหรับ การปรับโครงสร้าง นักพัฒนาสามารถเขียนการimplement ใหม่ ปรับประสิทธิภาพให้เหมาะสม หรือปรับปรุงความอ่านง่าย โดยมั่นใจว่าเทสจะตรวจจับความเบี่ยงเบนจากพฤติกรรมที่คาดหวังได้ทันที ในการพัฒนา Android โมบาย ขั้นตอนนี้สำคัญเป็นพิเศษสำหรับการแยกอินเทอร์เฟซร่วมและลดความซ้ำซ้อนของโค้ด
การนำ TDD ไปใช้ในโปรเจกต์โมบายให้ข้อดีที่วัดผลได้ ซึ่งได้รับการยืนยันทั้งจากการวิจัยทางวิชาการและการปฏิบัติของสตูดิโอพัฒนาชั้นนำ
การศึกษา IBM (Bhat & Nagappan, 2006) ในสี่โปรเจกต์อุตสาหกรรมแสดงให้เห็นว่าทีมที่ใช้ TDD สร้าง ข้อบกพร่องน้อยกว่า 40% เมื่อเทียบกับทีมที่คล้ายกันซึ่งทำงานด้วยแนวทางดั้งเดิม สำหรับการพัฒนาโมบาย ที่ค่าใช้จ่ายในการแก้ไขบั๊กหลังเผยแพร่บน Google Play สูงกว่าอย่างมีนัยสำคัญเมื่อเทียบกับขั้นตอนการเขียนโค้ด เมตริกนี้มีความสำคัญอย่างยิ่ง
เทสที่เขียนด้วย TDD ทำหน้าที่เป็น เอกสารที่มีชีวิต ของ API นักพัฒนาที่เข้ามาในโปรเจกต์สามารถอ่านเทสและเข้าใจว่าควรใช้แต่ละคอมโพเนนต์อย่างไร สิ่งนี้มีค่าโดยเฉพาะในสภาวะที่ทีมมีการเปลี่ยนแปลงสูง — ปัญหาทั่วไปของสตูดิโอโมบาย
ความครอบคลุมโค้ดที่เกิน 90% ช่วยให้นักพัฒนาสามารถปรับโครงสร้างได้โดยไม่ต้องกลัวว่าจะทำอะไรพัง Google ในหนังสือ “Software Engineering at Google” (2020) เรียกความครอบคลุมเทสว่าปัจจัยสำคัญที่ช่วยให้รักษาโค้ดเบสให้สะอาดในโปรเจกต์ที่มีโค้ดหลายล้านบรรทัด
ระบบนิเวศ TDD ในการพัฒนาโมบายมีเครื่องมือสำหรับการทดสอบหน่วย การสร้าง mock และการตรวจสอบคอมโพเนนต์ UI — ทั้งสำหรับ Android และ iOS
| เครื่องมือ | แพลตฟอร์ม | วัตถุประสงค์ |
|---|---|---|
| JUnit 5 | Android (Kotlin/Java) | เฟรมเวิร์กพื้นฐานสำหรับการทดสอบหน่วย |
| Mockito | Android | สร้างออบเจกต์ mock และตรวจสอบการเรียก |
| MockK | Android (Kotlin) | Mocking ด้วยไวยากรณ์ Kotlin-first และรองรับ coroutine |
| Turbine | Android | ทดสอบ Kotlin Flow และสตรีมเชิงปฏิกิริยา |
| XCTest | iOS (Swift) | เฟรมเวิร์กการทดสอบมาตรฐาน |
สำหรับโปรเจกต์ Android บน Kotlin สแต็กมาตรฐานประกอบด้วย JUnit 5 + MockK MockK ดีกว่า Mockito เพราะรองรับฟีเจอร์ระดับแรกของ Kotlin — sealed class, coroutine และ suspend function — โดยไม่ต้องตั้งค่าเพิ่มเติม
ในการพัฒนา iOS TDD ถูกimplement ผ่าน XCTest — เฟรมเวิร์กในตัวของ Apple ที่ให้ assertions, คลาสเทส และการรวม CI/CD ผ่าน Xcode Server หรือ GitHub Actions สำหรับ mocking บน iOS ใช้ไลบรารี Cuckoo และ OHHTTPStubs
มาดูสถานการณ์จริงของ TDD ใน Kotlin สำหรับ Android — การทดสอบ repository ผู้ใช้ ขั้นแรกเราเขียนเทส จากนั้น implement ที่ผ่านเทสนั้น
class UserRepositoryTest {
private val api = mockk<UserApi>()
private val dao = mockk<UserDao>()
private val repo = UserRepository(api, dao)
fun `when api returns user then cache and emit`() = runTest {
val user = User(1, "Alice")
coEvery { api.getUser(1) } returns user
every { dao.insert(user) } returns Unit
val result = repo.getUser(1)
assertEquals(user, result)
verify { dao.insert(user) }
}
}
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUser(id: Int): User {
val user = api.getUser(id)
dao.insert(user)
return user
}
}
หลังจากผ่านเทสแรก เราเพิ่มเทสที่สอง — ตรวจสอบพฤติกรรมเมื่อเกิด ข้อผิดพลาดเครือข่าย ตอนนี้เทสกำหนดว่าเมื่อ API ล้มเหลว repository ควรส่งคืนข้อมูลจากแคช
fun `when api fails then return cached user`() = runTest {
val cached = User(1, "Cached Alice")
coEvery { api.getUser(1) } throws IOException()
every { dao.getById(1) } returns cached
val result = repo.getUser(1)
assertEquals(cached, result)
}
การเปลี่ยนไปใช้ TDD มาพร้อมกับข้อผิดพลาดทั่วไปที่สามารถลบล้างข้อดีทั้งหมดของระเบียบวิธี การเข้าใจกับดักเหล่านี้ช่วยให้ทีมนำแนวปฏิบัติไปใช้ได้อย่างมีประสิทธิภาพมากขึ้น
รูปแบบต้านแรกและพบบ่อยที่สุด — การทดสอบฟังก์ชันการทำงานที่มากเกินไป ในเทสเดียว เทสควรตรวจสอบเพียงหนึ่งการยืนยัน หากเทสล้มเหลว นักพัฒนาควรรู้ทันทีว่าอะไรพังโดยไม่ต้องดีบักเพิ่มเติม
ข้อผิดพลาดที่สอง — การเขียนเทสที่ผ่านตั้งแต่แรก หากเทสไม่เคยเป็นสีแดงแม้แต่ครั้งเดียว ก็ไม่มีความมั่นใจว่ามันกำลังตรวจสอบบางสิ่งจริงๆ กฎ: อย่าเชื่อถือเทสที่คุณไม่เคยเห็นมันล้มเหลว
ข้อผิดพลาดทั่วไปที่สาม — การหยุดที่ขั้นตอนเขียว การปรับโครงสร้าง ไม่ใช่ทางเลือก แต่เป็นขั้นตอนบังคับของวงจร หากไม่มีมัน โค้ดเบสจะเสื่อมลง เทสจะเปราะบาง และข้อดีของ TDD จะสูญหายไป
คำถามที่พบบ่อย
TDD เป็นเทคนิคการออกแบบเป็นหลัก ไม่ใช่เทคนิคการทดสอบ เทสใน TDD มีบทบาทเป็นข้อกำหนด: มันกำหนด API ของคอมโพเนนต์ก่อนการimplement Kent Beck เองเรียก TDD ว่า “เป็นวินัยของการออกแบบ ไม่ใช่การทดสอบ”
ตามการศึกษาของ Microsoft Research ทีมต้องใช้เวลา 3 ถึง 6 เดือนของการปฏิบัติอย่างต่อเนื่องเพื่อให้ TDD กลายเป็นนิสัย 2–3 สัปดาห์แรก ประสิทธิภาพลดลง 15–30% แต่หลังจากการปรับตัว มันกลับมาที่ระดับเดิมหรือเกินกว่าเนื่องจากเวลาในการดีบักที่ลดลง
ใช่ แต่มีข้อจำกัด สำหรับ ตรรกะ UI (ViewModel, State) TDD สามารถใช้ได้โดยตรง สำหรับคอมโพเนนต์ภาพ (Compose UI, SwiftUI Views) การทดสอบภาพจับ (snapshot testing) เสริม TDD แต่ไม่ได้แทนที่ แนะนำให้แยกตรรกะธุรกิจออกจากการแสดงผล
สำหรับ โค้ดเดิม แนะนำกลยุทธ์ “เทสลักษณะเฉพาะ” (characterization tests) — ที่เขียนเทสบนพฤติกรรมที่มีอยู่ จากนั้นปรับโครงสร้างโค้ด แนวทางนี้อธิบายไว้ในหนังสือของ Michael Feathers “Working Effectively with Legacy Code” (2004) และอนุญาตให้นำ TDD ไปใช้แบบทีละขั้น
TDD และ Clean Architecture เสริมสร้างซึ่งกันและกัน สถาปัตยกรรมที่สะอาดต้องการขอบเขตที่ชัดเจนระหว่างเลเยอร์ และ TDD บังคับให้นักพัฒนาออกแบบขอบเขตเหล่านั้นผ่านเทส เลเยอร์โดเมนถูกทดสอบแบบแยกส่วนด้วย dependencies แบบ mock เลเยอร์ข้อมูล — ผ่านเทสการรวมระบบ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม