การทดสอบหน่วยคือวิธีการตรวจสอบซอฟต์แวร์ที่ทดสอบความถูกต้องของโมดูลหรือฟังก์ชันโค้ดแต่ละส่วนอย่างแยกอิสระจากระบบที่เหลือ ตามข้อมูลจาก Martin Fowler, 2026 การทดสอบหน่วยเป็นรากฐานของ CI/CD และการปรับโครงสร้างโค้ด โดยให้ข้อเสนอแนะที่รวดเร็วเกี่ยวกับความถูกต้องของโค้ด การทดสอบโมดูล ช่วยตรวจจับข้อผิดพลาดในระยะเริ่มต้นของการพัฒนา ซึ่งลดต้นทุนการแก้ไขผิดพลาดลงหลายเท่า
ประเด็นสำคัญ
การทดสอบหน่วยคือกระบวนการตรวจสอบ หน่วยแต่ละหน่วย ของซอร์สโค้ด — ฟังก์ชัน เมธอด คลาส — แยกอิสระจากส่วนที่เหลือของโปรแกรม การทดสอบแต่ละครั้งจะรันสถานการณ์การใช้งานเฉพาะของโมดูลและตรวจสอบว่าผลลัพธ์ตรงกับที่คาดหวังหรือไม่ การทดสอบหน่วยเขียนด้วยภาษาโปรแกรมมิ่งเดียวกันกับโค้ดหลักและทำงานอัตโนมัติในสภาพแวดล้อมการพัฒนาหรือในไปป์ไลน์ CI/CD ต่างจากการทดสอบแบบบูรณาการ การทดสอบหน่วย จะไม่โต้ตอบ กับฐานข้อมูลจริง ระบบไฟล์ หรือบริการเครือข่าย
เป้าหมายหลักคือ ข้อเสนอแนะที่รวดเร็ว เกี่ยวกับความถูกต้องของโค้ดหลังการเปลี่ยนแปลง หากนักพัฒนาปรับโครงสร้างเมธอด ชุดการทดสอบหน่วยจะยืนยันว่าพฤติกรรมไม่เสียหาย ตาม Google Testing Blog (2025) โปรเจกต์ที่มีความครอบคลุมการทดสอบหน่วยสูงกว่า 60% มีเหตุการณ์ในระบบผลิตน้อยกว่า 2.5 เท่า ประโยชน์เพิ่มเติม: เอกสารประกอบโค้ด (การทดสอบแสดงวิธีใช้ API) การปรับโครงสร้างที่ง่ายขึ้น (สามารถเปลี่ยนการนำไปใช้ได้ในขณะที่ยังคงพฤติกรรม) และการวินิจฉัยการถดถอยที่รวดเร็ว
ไม่ใช่ทุกการทดสอบอัตโนมัติจะเป็นการทดสอบหน่วย เกณฑ์: มีการทดสอบ โมดูลเดียว (คลาสหรือฟังก์ชัน) การพึ่งพาภายนอกถูกแทนที่ด้วยม็อกหรือสตับ การทดสอบทำงานในหน่วยมิลลิวินาที และไม่ต้องเริ่มเซิร์ฟเวอร์หรือฐานข้อมูล การทดสอบที่เข้าถึงฐานข้อมูลจริงคือการทดสอบแบบบูรณาการ การทดสอบที่เปิดเบราว์เซอร์คือการทดสอบ E2E การทำความเข้าใจขอบเขตระหว่างประเภทการทดสอบมีความสำคัญสำหรับ การกระจายความพยายาม อย่างถูกต้องในพีระมิดการทดสอบ
การทดสอบหน่วยที่มีคุณภาพเป็นไปตามหลักการ FIRST ที่กำหนดโดย Robert C. Martin การทดสอบแต่ละครั้งควร Fast (รวดเร็ว — มิลลิวินาที), Isolated (แยกอิสระ — ไม่ขึ้นอยู่กับการทดสอบอื่น), Repeatable (ทำซ้ำได้ — ผลลัพธ์เดียวกันบนเครื่องใดก็ได้), Self-validating (ตรวจสอบตัวเอง — ผลลัพธ์ "ผ่าน" หรือ "ไม่ผ่าน" โดยไม่ต้องตรวจสอบด้วยตนเอง) และ Timely (ทันเวลา — เขียนก่อนหรือพร้อมกันกับโค้ด) การละเมิดหลักการใดก็ตามจะลดคุณค่าของการทดสอบ
แม่แบบมาตรฐานสำหรับการเขียนการทดสอบหน่วย Arrange — เตรียมข้อมูลและการพึ่งพา: สร้างอ็อบเจกต์ กำหนดค่าม็อก ตั้งค่าพารามิเตอร์นำเข้า Act — ดำเนินการกระทำที่ทดสอบ: เรียกเมธอดหรือฟังก์ชัน Assert — ตรวจสอบผลลัพธ์: เปรียบเทียบค่าจริงกับค่าที่คาดหวัง การแบ่งเป็นสามบล็อกทำให้การทดสอบอ่านง่ายและเข้าใจได้ หากบล็อก Assert ต้องการตรรกะที่ซับซ้อน แสดงว่าการทดสอบกำลังตรวจสอบมากเกินไปในครั้งเดียว
// ตัวอย่างการทดสอบหน่วยด้วยรูปแบบ AAA ใน Kotlin กับ JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — สร้างอ็อบเจกต์ที่ทดสอบ
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — ดำเนินการ
val result = calculator.add(2, 3)
// ASSERT — ตรวจสอบผลลัพธ์
Assertions.assertEquals(5, result)
}
}
ชื่อการทดสอบควรอธิบายสิ่งที่กำลังตรวจสอบและผลลัพธ์ที่คาดหวัง รูปแบบ: [methodName]_[scenario]_[expectedResult] ตัวอย่าง: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal ชื่อการทดสอบที่ดีแทนที่ความคิดเห็นและเมื่อล้มเหลวจะบอกทันทีว่าฟังก์ชันใดเสียหาย หลีกเลี่ยงชื่อเช่น test1, checkSomething หรือ verify — ชื่อเหล่านี้ไม่มีข้อมูลและทำให้การวินิจฉัยซับซ้อน
เพื่อแยกโมดูลที่ทดสอบจากการพึ่งพาภายนอก จะใช้ตัวจำลองการทดสอบ (test doubles) ประเภทหลัก: ม็อก (mocks) — ตรวจสอบว่าเมธอดที่ระบุถูกเรียกด้วยพารามิเตอร์ที่คาดหวัง; สตับ (stubs) — ส่งคืนค่าที่กำหนดไว้ล่วงหน้าเมื่อเรียกเมธอด; เฟก (fakes) — การนำไปใช้อย่างง่ายของส่วนประกอบจริง (เช่น InMemoryUserRepository แทน UserRepository ที่ทำงานกับฐานข้อมูล) การเลือกขึ้นอยู่กับสิ่งที่ต้องตรวจสอบ: สถานะ (สตับ) หรือการโต้ตอบ (ม็อก)
| ตัวจำลอง | ตรวจสอบอะไร | ตัวอย่าง |
|---|---|---|
| ม็อก | เรียกเมธอดด้วยพารามิเตอร์ที่ถูกต้อง | userRepository.save(user) ถูกเรียก 1 ครั้งพอดี |
| สตับ | ค่าที่ส่งคืน | repository.findById(1) ส่งคืน User(id=1, name="Test") |
| เฟก | ตรรกะผ่านการนำไปใช้อย่างง่าย | InMemoryMapUserRepository ด้วย HashMap แทนฐานข้อมูล |
| สปาย | การจำลองบางส่วนของอ็อบเจกต์จริง | spy(repo).when(findById).thenReturn(user) |
Mockito เป็นเฟรมเวิร์กการจำลองที่ได้รับความนิยมมากที่สุดสำหรับ Java และ Kotlin ช่วยให้สร้างม็อกผ่าน mock() กำหนดค่าที่ส่งคืนผ่าน when().thenReturn() และตรวจสอบการเรียกผ่าน verify() Mockito เวอร์ชันใหม่ (5.x) รองรับการจำลองแบบสแตติก (mockStatic) และไวยากรณ์ที่ง่ายขึ้นผ่าน BDDMockito (given-willReturn) กฎสำคัญ: อย่าจำลองสิ่งที่คุณไม่ได้เป็นเจ้าของ — อย่าสร้างม็อกสำหรับอ็อบเจกต์ค่าและไลบรารีมาตรฐาน
// ตัวอย่างการทดสอบหน่วยด้วย Mockito ใน Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) เป็นวิธีการที่เขียนการทดสอบ ก่อน การนำโค้ดไปใช้ วงจร "แดง-เขียว-ปรับโครงสร้าง": เขียนการทดสอบที่ล้มเหลว (แดง) เขียนโค้ดน้อยที่สุดเพื่อให้การทดสอบผ่าน (เขียว) ปรับปรุงโค้ดโดยไม่เปลี่ยนพฤติกรรม (ปรับโครงสร้าง) TDD รับประกันว่าโค้ดทั้งหมดถูกครอบคลุมโดยการทดสอบ (ความครอบคลุม = 100% สำหรับฟังก์ชันที่นำไปใช้) และโค้ดสามารถทดสอบได้ — หากโค้ดทดสอบยาก แสดงว่าสถาปัตยกรรมต้องปรับปรุง
จากการวิจัยของ IBM (2006-2026, การศึกษาระยะยาว) ทีมที่ใช้ TDD มีข้อบกพร่องในระบบผลิตน้อยกว่า 40-80% เมื่อเทียบกับทีมที่เขียนการทดสอบหลังโค้ด TDD ยังปรับปรุงสถาปัตยกรรม: นักพัฒนาถูกบังคับให้คิดเกี่ยวกับการออกแบบ API ก่อนการนำไปใช้ ซึ่งนำไปสู่การเชื่อมโยงแบบหลวม (loose coupling) และการเกาะกลุ่มกันสูง (high cohesion) ผลกระทบเพิ่มเติมคือ เอกสารที่มีชีวิต: การทดสอบทำหน้าที่เป็นข้อมูลจำเพาะของพฤติกรรมโมดูลที่ทันสมัยเสมอ
TDD ไม่เหมาะสมเสมอไป ส่วนประกอบ UI ทดสอบแยกกันยาก — การทดสอบสแนปชอตหรือการทดสอบการถดถอยด้วยภาพ (Percy, Chromatic) มีประสิทธิภาพมากกว่า การทำต้นแบบและการวิจัย (spike solutions) ไม่ต้องการการทดสอบ โค้ดเดิมที่ไม่มีการทดสอบยากที่จะครอบคลุมผ่าน TDD — ในที่นี้อันดับแรกต้องมี การทดสอบลักษณะเฉพาะ (การทดสอบที่บันทึกพฤติกรรมปัจจุบันก่อนการปรับโครงสร้าง) ในกรณีเหล่านี้ TDD จะไม่ถูกยกเลิกโดยสิ้นเชิงแต่ถูกปรับเปลี่ยน — เขียนการทดสอบสำหรับฟังก์ชันที่เปลี่ยนแปลง ไม่ใช่สำหรับโค้ดเดิมทั้งหมด
การพัฒนาโมบายล์มีลักษณะเฉพาะ: ตรรกะทางธุรกิจมักผสมกับโค้ด UI (Activity, ViewController, ViewModel) ซึ่งทำให้การทดสอบหน่วยซับซ้อน แนวปฏิบัติที่ดีที่สุดคือ View แบบบาง, ViewModel แบบหนา: แยกตรรกะทั้งหมดจากส่วนประกอบ UI ไปยังคลาสแยก (UseCase, Repository, ViewModel) ที่ทดสอบได้ง่ายโดยไม่ต้องใช้โปรแกรมจำลอง Android และ iOS มีเฟรมเวิร์กการทดสอบหน่วยดั้งเดิมที่ทำงานบน JVM/Native โดยไม่ต้องเปิดอุปกรณ์
การทดสอบหน่วย Android ทำงานบน JVM ในเครื่องโดยไม่ต้องใช้โปรแกรมจำลอง ให้ ความเร็วในการดำเนินการ — การทดสอบทั่วไปใช้เวลาน้อยกว่า 100ms JUnit 5 เป็นตัวรันหลัก สำหรับการทดสอบ ViewModel ใช้ kotlinx-coroutines-test สำหรับการทดสอบโคโรทีนและ Turbine สำหรับการทดสอบ StateFlow Robolectric ช่วยให้ทดสอบส่วนประกอบที่พึ่งพา Android (Context, Resources) โดยไม่ต้องใช้โปรแกรมจำลองโดยการโหลดคลาสเงา สำหรับการทดสอบ Compose ใช้ Compose UI Test — แต่สิ่งเหล่านี้คือการทดสอบ UI ไม่ใช่การทดสอบหน่วย
การทดสอบหน่วย iOS เขียนด้วย Swift ด้วย XCTest (ในตัวของ Xcode) Quick + Nimble เป็นเฟรมเวิร์ก BDD สำหรับการทดสอบที่อ่านง่ายขึ้น (describe/context/it) สำหรับการจำลอง ใช้ Cuckoo (สร้างม็อก) หรือ SwiftyMocky Swift รองรับโปรโตคอลและการฉีดพึ่งพา ซึ่งทำให้การเปลี่ยนพึ่งพาทำได้ง่ายขึ้น ประเด็นสำคัญ: การทดสอบหน่วย iOS ทำงานบนโปรแกรมจำลอง macOS ไม่ใช่บนอุปกรณ์จริง การทดสอบที่ต้องใช้คุณสมบัติฮาร์ดแวร์ (กล้อง บลูทูธ) คือการทดสอบแบบบูรณาการ
การทดสอบหน่วย Flutter ใช้แพ็คเกจ flutter_test และทำงานบน Dart VM โดยไม่ต้องใช้โปรแกรมจำลอง สำหรับการจำลอง ใช้แพ็คเกจ mockito กับการสร้างโค้ด (build_runner) การทดสอบวิดเจ็ต (ในแพ็คเกจเดียวกัน) ทดสอบวิดเจ็ตแต่ละตัวแต่ต้องเรนเดอร์และทำงานช้ากว่า — ใช้เฉพาะสำหรับการตรวจสอบตรรกะ UI ตรรกะ Dart บริสุทธิ์ (โมเดล พื้นที่เก็บข้อมูล บล็อก) จะถูกทดสอบเป็นการทดสอบ Dart ปกติโดยไม่ต้องนำเข้า flutter_test
// ตัวอย่างการทดสอบหน่วยบน Flutter ด้วย mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
การทดสอบหน่วยที่มีประสิทธิภาพต้องมีวินัย กฎหลัก: ทดสอบพฤติกรรม ไม่ใช่การนำไปใช้ การทดสอบไม่ควรทราบว่าโมดูลถูกนำไปใช้ภายในอย่างไร (เมธอดส่วนตัวใดถูกเรียกในลำดับใด) หากการทดสอบยึดติดกับการนำไปใช้ การทดสอบจะเสียหายทุกครั้งที่มีการปรับโครงสร้างและสูญเสียคุณค่า การทดสอบตรวจสอบสัญญา: ด้วยอินพุต X เอาต์พุตควรเป็น Y ข้อยกเว้นคือการทดสอบสำหรับอัลกอริทึมที่มีประสิทธิภาพสำคัญ ซึ่งลำดับการเรียกมีความสำคัญ
ความครอบคลุม 100% เป็นเป้าหมายที่ไม่สามารถบรรลุได้และไม่จำเป็น ตาม Google Testing Blog (2025) ระดับความครอบคลุมที่เหมาะสมที่สุดสำหรับการทดสอบหน่วยคือ 70-80% ของบรรทัดโค้ด ความครอบคลุม 100% มักทำได้โดยการทดสอบ getter, setter และคอนสตรัคเตอร์ ซึ่งไม่ได้เพิ่มคุณค่า มุ่งเน้นไปที่ ตรรกะทางธุรกิจที่สำคัญ: การคำนวณที่ซับซ้อน การตรวจสอบ การจัดการข้อผิดพลาด กรณีขอบ ใช้ JaCoCo (Java), Coverage.py (Python), Istanbul (JS) สำหรับการวัดและกำหนดเกณฑ์ใน CI — การสร้างล้มเหลวเมื่อความครอบคลุมต่ำกว่า 60%
การทดสอบหน่วยเป็น ขั้นตอนแรก ของไปป์ไลน์ CI/CD ใด ๆ การทดสอบเหล่านี้ทำงานทุกครั้งที่พุชไปยังพื้นที่เก็บข้อมูล ก่อนการสร้างและการปรับใช้ เวลาทำงานเฉลี่ยของชุดการทดสอบหน่วยไม่ควรเกิน 5 นาที — หากนานกว่านั้น การทดสอบจะไม่ "รวดเร็ว" อีกต่อไปและนักพัฒนาจะหยุดรันในเครื่อง แยกการทดสอบเป็นเร็ว (หน่วย) และช้า (บูรณาการ) และรันในขั้นตอนต่างๆ ของไปป์ไลน์ ใช้ การดำเนินการแบบขนาน และการล้มเหลวเร็วเพื่อความเร็ว
คำถามที่พบบ่อย
การทดสอบหน่วยตรวจสอบ โมดูลเดียวแบบแยกอิสระ โดยแทนที่การพึ่งพาภายนอกด้วยม็อก การทดสอบแบบบูรณาการตรวจสอบการโต้ตอบระหว่างส่วนประกอบจริงหลายตัว (ฐานข้อมูล API ระบบไฟล์) การทดสอบหน่วยทำงานในหน่วยมิลลิวินาที การทดสอบแบบบูรณาการทำงานในหน่วยวินาที ในพีระมิดการทดสอบ การทดสอบหน่วยครอบครอง 70%
การเลือกขึ้นอยู่กับแพลตฟอร์ม: JUnit 5 สำหรับ Java/Kotlin, XCTest สำหรับ iOS/Swift, pytest สำหรับ Python, Jest/Vitest สำหรับ JavaScript/TypeScript, flutter_test สำหรับ Flutter สำหรับการจำลอง ใช้ Mockito (Java), Cuckoo (iOS), unittest.mock (Python) หรือ vitest.mock (JS) เฟรมเวิร์กสมัยใหม่ทั้งหมดรองรับการทดสอบแบบกำหนดพารามิเตอร์ การยืนยันในตัว และการดำเนินการแบบขนาน
Fast — การทดสอบทำงานในหน่วยมิลลิวินาที Isolated — ไม่ขึ้นอยู่กับการทดสอบอื่นหรือระบบภายนอก Repeatable — ให้ผลลัพธ์เดียวกันบนเครื่องใดก็ได้ Self-validating — ตรวจสอบผลลัพธ์โดยอัตโนมัติ Timely — เขียนก่อนหรือพร้อมกันกับโค้ด การละเมิดหลักการเดียวก็ลดประสิทธิภาพการทดสอบ
ใช่ จำเป็นอย่างยิ่ง ViewModel มี ตรรกะทางธุรกิจ — การจัดการเหตุการณ์ การแปลงข้อมูล การจัดการสถานะ บน Android ใช้ kotlinx-coroutines-test สำหรับโคโรทีนและ Turbine สำหรับการทดสอบ StateFlow บน iOS ทดสอบ Combine Publishers หรือ async/await ใน ViewModel การทดสอบ ViewModel คือการทดสอบหน่วยบริสุทธิ์ที่ทำงานบน JVM/macOS โดยไม่ต้องใช้โปรแกรมจำลอง
คำขอเครือข่ายในการทดสอบหน่วย ไม่ถูกดำเนินการ — จะถูกแทนที่ด้วยม็อกของไคลเอ็นต์ HTTP บน Android ใช้ MockWebServer (OkHttp) — มันเริ่มเซิร์ฟเวอร์ HTTP ในเครื่อง ซึ่งดีกว่าม็อกเพราะจำลองการโต้ตอบเครือข่ายจริง MockWebServer ให้การแยกอิสระโดยไม่สูญเสียความสมจริง สำหรับ iOS — ใช้ OHHTTPStubs หรือ URLProtocol สำหรับการสกัดกั้นและแทนที่การตอบสนอง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม