Kiểm thử đơn vị là phương pháp xác minh phần mềm trong đó tính đúng đắn của các mô-đun hoặc hàm mã riêng lẻ được kiểm tra một cách độc lập với phần còn lại của hệ thống. Theo Martin Fowler, 2026, kiểm thử đơn vị là nền tảng của CI/CD và tái cấu trúc, cung cấp phản hồi nhanh về hoạt động của mã. Kiểm thử mô-đun giúp phát hiện lỗi ở giai đoạn đầu của quá trình phát triển, giảm chi phí sửa lỗi nhiều lần.
Các điểm chính
Kiểm thử đơn vị là quá trình xác minh các đơn vị riêng lẻ của mã nguồn — hàm, phương thức, lớp — một cách độc lập với phần còn lại của chương trình. Mỗi kiểm thử chạy một kịch bản sử dụng cụ thể của mô-đun và kiểm tra xem kết quả có khớp với kỳ vọng không. Kiểm thử đơn vị được viết bằng cùng ngôn ngữ lập trình với mã chính và chạy tự động trong môi trường phát triển hoặc trong đường ống CI/CD. Không giống như kiểm thử tích hợp, kiểm thử đơn vị không tương tác với cơ sở dữ liệu thực, hệ thống tệp hoặc dịch vụ mạng.
Mục tiêu chính là phản hồi nhanh về tính đúng đắn của mã sau khi thay đổi. Nếu nhà phát triển tái cấu trúc một phương thức, bộ kiểm thử đơn vị xác nhận rằng hành vi không bị phá vỡ. Theo Google Testing Blog (2025), các dự án có độ phủ kiểm thử đơn vị trên 60% có số lượng sự cố sản xuất ít hơn 2,5 lần. Lợi ích bổ sung: tài liệu mã (kiểm thử cho thấy cách sử dụng API), đơn giản hóa tái cấu trúc (có thể thay đổi triển khai trong khi vẫn giữ nguyên hành vi) và chẩn đoán hồi quy nhanh.
Không phải mọi kiểm thử tự động đều là kiểm thử đơn vị. Tiêu chí: một mô-đun duy nhất (lớp hoặc hàm) được kiểm tra, các phụ thuộc bên ngoài được thay thế bằng mock hoặc stub, kiểm thử chạy trong mili giây và không yêu cầu khởi động máy chủ hoặc cơ sở dữ liệu. Kiểm thử truy cập cơ sở dữ liệu thực là kiểm thử tích hợp. Kiểm thử mở trình duyệt là kiểm thử E2E. Hiểu ranh giới giữa các loại kiểm thử rất quan trọng để phân bổ nỗ lực hợp lý trong kim tự tháp kiểm thử.
Kiểm thử đơn vị chất lượng tuân theo các nguyên tắc FIRST do Robert C. Martin xây dựng. Mỗi kiểm thử nên Fast (nhanh — mili giây), Isolated(độc lập — không phụ thuộc vào kiểm thử khác), Repeatable (tái tạo — cùng kết quả trên mọi máy), Self-validating (tự xác minh — kết quả "đạt" hoặc "không đạt", không cần kiểm tra thủ công) và Timely (kịp thời — viết trước hoặc đồng thời với mã). Vi phạm bất kỳ nguyên tắc nào cũng làm giảm giá trị của kiểm thử.
Một mẫu tiêu chuẩn để viết kiểm thử đơn vị. Arrange — chuẩn bị dữ liệu và phụ thuộc: tạo đối tượng, cấu hình mock, thiết lập tham số đầu vào. Act — thực hiện hành động được kiểm tra: gọi phương thức hoặc hàm. Assert — xác minh kết quả: so sánh giá trị thực tế với giá trị kỳ vọng. Việc chia thành ba khối làm cho kiểm thử dễ đọc và dễ hiểu. Nếu khối Assert yêu cầu logic phức tạp, kiểm thử có thể đang kiểm tra quá nhiều cùng một lúc.
// Ví dụ kiểm thử đơn vị với mẫu AAA trong Kotlin với JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — tạo đối tượng kiểm thử
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — thực hiện hành động
val result = calculator.add(2, 3)
// ASSERT — xác minh kết quả
Assertions.assertEquals(5, result)
}
}
Tên kiểm thử nên mô tả những gì đang được kiểm tra và kết quả mong đợi. Định dạng: [methodName]_[scenario]_[expectedResult]. Ví dụ: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Một tên kiểm thử tốt thay thế nhận xét và khi thất bại sẽ chỉ ra ngay chức năng nào bị hỏng. Tránh các tên như test1, checkSomething hoặc verify — chúng không mang thông tin và gây khó khăn cho chẩn đoán.
Để cô lập mô-đun được kiểm tra khỏi các phụ thuộc bên ngoài, người ta sử dụng các bản sao kiểm thử (test doubles). Các loại chính: mock — xác minh rằng một phương thức cụ thể đã được gọi với các tham số mong đợi; stub — trả về các giá trị được xác định trước khi gọi phương thức; fake — triển khai đơn giản hóa của các thành phần thực (ví dụ: InMemoryUserRepository thay vì UserRepository làm việc với cơ sở dữ liệu). Việc lựa chọn phụ thuộc vào những gì cần xác minh: trạng thái (stub) hay tương tác (mock).
| Bản sao | Xác minh gì | Ví dụ |
|---|---|---|
| Mock | Gọi phương thức với tham số đúng | userRepository.save(user) được gọi đúng 1 lần |
| Stub | Giá trị trả về | repository.findById(1) trả về User(id=1, name="Test") |
| Fake | Logic qua triển khai đơn giản hóa | InMemoryMapUserRepository với HashMap thay vì DB |
| Spy | Mock một phần đối tượng thực | spy(repo).when(findById).thenReturn(user) |
Mockito là framework mocking phổ biến nhất cho Java và Kotlin. Nó cho phép tạo mock qua mock(), cấu hình giá trị trả về qua when().thenReturn() và xác minh lời gọi qua verify(). Các phiên bản hiện đại của Mockito (5.x) hỗ trợ static mock (mockStatic) và cú pháp đơn giản hóa qua BDDMockito (given-willReturn). Một quy tắc quan trọng: đừng mock những gì không phải của bạn — không tạo mock cho các đối tượng giá trị và thư viện chuẩn.
// Ví dụ kiểm thử đơn vị với Mockito trong 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) là phương pháp luận trong đó kiểm thử được viết trước khi triển khai mã. Chu trình "Đỏ-Xanh-Tái cấu trúc": viết kiểm thử thất bại (Đỏ), viết mã tối thiểu để vượt qua kiểm thử (Xanh), cải thiện mã mà không thay đổi hành vi (Tái cấu trúc). TDD đảm bảo rằng tất cả mã đều được kiểm thử bao phủ (độ phủ = 100% cho chức năng đã triển khai) và mã có thể kiểm thử được — nếu mã khó kiểm tra, kiến trúc cần cải thiện.
Theo nghiên cứu của IBM (2006-2026, nghiên cứu dọc), các nhóm sử dụng TDD có ít lỗi sản xuất hơn 40-80% so với các nhóm viết kiểm thử sau mã. TDD cũng cải thiện kiến trúc: nhà phát triển buộc phải suy nghĩ về thiết kế API trước khi triển khai, dẫn đến sự kết hợp lỏng lẻo (loose coupling) và tính kết dính cao (high cohesion). Một hiệu ứng bổ sung là tài liệu sống: kiểm thử đóng vai trò là đặc tả hành vi của mô-đun, luôn được cập nhật.
TDD không phải lúc nào cũng tối ưu. Các thành phần UI khó kiểm tra độc lập — kiểm thử snapshot hoặc kiểm thử hồi quy hình ảnh (Percy, Chromatic) hiệu quả hơn. Tạo mẫu thử và nghiên cứu (spike solutions) không yêu cầu kiểm thử. Mã kế thừa không có kiểm thử khó bao phủ qua TDD — ở đây trước tiên cần kiểm thử đặc tính (kiểm thử ghi lại hành vi hiện tại trước khi tái cấu trúc). Trong những trường hợp này, TDD không bị từ bỏ hoàn toàn mà được điều chỉnh — kiểm thử được viết cho chức năng đã thay đổi, không phải cho toàn bộ mã kế thừa.
Phát triển di động có tính đặc thù: logic nghiệp vụ thường bị trộn lẫn với mã UI (Activity, ViewController, ViewModel), gây khó khăn cho kiểm thử đơn vị. Thực hành tốt nhất là View mỏng, ViewModel dày: trích xuất tất cả logic từ các thành phần UI vào các lớp riêng biệt (UseCase, Repository, ViewModel) dễ kiểm tra mà không cần trình giả lập. Android và iOS có framework kiểm thử đơn vị gốc chạy trên JVM/Native mà không cần khởi động thiết bị.
Kiểm thử đơn vị Android chạy trên JVM cục bộ mà không cần trình giả lập, mang lại tốc độ thực thi — một kiểm thử điển hình mất dưới 100ms. JUnit 5 là trình chạy chính. Đối với kiểm thử ViewModel, sử dụng kotlinx-coroutines-test để kiểm tra coroutine và Turbine để kiểm tra StateFlow. Robolectric cho phép kiểm tra các thành phần phụ thuộc Android (Context, Resources) mà không cần trình giả lập bằng cách tải các lớp shadow. Đối với kiểm thử Compose, sử dụng Compose UI Test — nhưng đây là kiểm thử UI, không phải kiểm thử đơn vị.
Kiểm thử đơn vị iOS được viết bằng Swift với XCTest (tích hợp trong Xcode). Quick + Nimble là framework BDD cho kiểm thử dễ đọc hơn (describe/context/it). Để mocking, sử dụng Cuckoo (tạo mock) hoặc SwiftyMocky. Swift hỗ trợ giao thức và tiêm phụ thuộc, giúp dễ dàng thay thế các phụ thuộc. Điểm chính: kiểm thử đơn vị iOS chạy trên trình giả lập macOS, không phải trên thiết bị thực. Kiểm thử yêu cầu tính năng phần cứng (camera, Bluetooth) là kiểm thử tích hợp.
Kiểm thử đơn vị Flutter sử dụng gói flutter_test và chạy trên Dart VM mà không cần trình giả lập. Để mocking, sử dụng gói mockito với tính năng tạo mã (build_runner). Kiểm thử widget (trong cùng gói) kiểm tra các widget riêng lẻ nhưng yêu cầu kết xuất và chạy chậm hơn — chỉ sử dụng chúng để xác minh logic UI. Logic Dart thuần (mô hình, kho lưu trữ, bloc) được kiểm tra như kiểm thử Dart thông thường mà không cần nhập flutter_test.
// Ví dụ kiểm thử đơn vị trên Flutter với 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);
});
}
Kiểm thử đơn vị hiệu quả đòi hỏi kỷ luật. Quy tắc chính: kiểm tra hành vi, không phải triển khai. Kiểm thử không nên biết mô-đun được triển khai nội bộ như thế nào (phương thức riêng nào được gọi, theo thứ tự nào). Nếu kiểm thử gắn với triển khai, nó sẽ hỏng sau mỗi lần tái cấu trúc và mất giá trị. Kiểm thử xác minh hợp đồng: với đầu vào X, đầu ra phải là Y. Ngoại lệ là kiểm thử cho các thuật toán có hiệu suất quan trọng, nơi thứ tự gọi là quan trọng.
Bao phủ 100% là mục tiêu không thể đạt được và không cần thiết. Theo Google Testing Blog (2025), mức bao phủ tối ưu cho kiểm thử đơn vị là 70-80% số dòng mã. Bao phủ 100% thường đạt được bằng cách kiểm tra getter, setter và hàm tạo, điều này không mang lại giá trị. Tập trung vào logic nghiệp vụ quan trọng: tính toán phức tạp, xác thực, xử lý lỗi, trường hợp biên. Sử dụng JaCoCo (Java), Coverage.py (Python), Istanbul (JS) để đo lường và đặt ngưỡng trong CI — xây dựng thất bại khi độ bao phủ dưới 60%.
Kiểm thử đơn vị là giai đoạn đầu tiên của bất kỳ đường ống CI/CD nào. Chúng chạy ở mỗi lần đẩy lên kho lưu trữ, trước khi xây dựng và triển khai. Thời gian chạy trung bình của bộ kiểm thử đơn vị không được vượt quá 5 phút — nếu lâu hơn, kiểm thử không còn "nhanh" và nhà phát triển sẽ ngừng chạy chúng cục bộ. Phân tách kiểm thử thành nhanh (đơn vị) và chậm (tích hợp) và chạy chúng trong các giai đoạn khác nhau của đường ống. Sử dụng thực thi song song và fail-fast để tăng tốc.
Câu hỏi thường gặp
Kiểm thử đơn vị xác minh một mô-đun duy nhất một cách độc lập, thay thế các phụ thuộc bên ngoài bằng mock. Kiểm thử tích hợp xác minh sự tương tác giữa nhiều thành phần thực (DB, API, hệ thống tệp). Kiểm thử đơn vị chạy trong mili giây, kiểm thử tích hợp chạy trong giây. Trong kim tự tháp kiểm thử, kiểm thử đơn vị chiếm 70%.
Việc lựa chọn phụ thuộc vào nền tảng: JUnit 5 cho Java/Kotlin, XCTest cho iOS/Swift, pytest cho Python, Jest/Vitest cho JavaScript/TypeScript, flutter_test cho Flutter. Để mocking, sử dụng Mockito (Java), Cuckoo (iOS), unittest.mock (Python) hoặc vitest.mock (JS). Tất cả framework hiện đại đều hỗ trợ kiểm thử tham số hóa, xác nhận tích hợp và thực thi song song.
Fast — kiểm thử chạy trong mili giây. Isolated — không phụ thuộc vào kiểm thử khác hoặc hệ thống bên ngoài. Repeatable — cho cùng kết quả trên mọi máy. Self-validating — tự động xác minh kết quả. Timely — viết trước hoặc đồng thời với mã. Vi phạm dù chỉ một nguyên tắc cũng làm giảm hiệu quả kiểm thử.
Có, chắc chắn rồi. ViewModel chứa logic nghiệp vụ — xử lý sự kiện, chuyển đổi dữ liệu, quản lý trạng thái. Trên Android, sử dụng kotlinx-coroutines-test cho coroutine và Turbine để kiểm tra StateFlow. Trên iOS, kiểm tra Combine Publishers hoặc async/await trong ViewModel. Kiểm thử ViewModel là kiểm thử đơn vị thuần túy chạy trên JVM/macOS mà không cần trình giả lập.
Các yêu cầu mạng trong kiểm thử đơn vị không được thực thi — chúng được thay thế bằng mock của máy khách HTTP. Trên Android, sử dụng MockWebServer (OkHttp) — nó chạy máy chủ HTTP cục bộ, được ưu tiên hơn mock vì tái tạo tương tác mạng thực. MockWebServer cung cấp sự độc lập mà không mất tính thực tế. Cho iOS — OHHTTPStubs hoặc URLProtocol để chặn và thay thế phản hồi.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm