Kiểm thử đơn vị trong phát triển di động: nó là gì, phương pháp và framework

Tác giả: IT Sectr Đã đăng: 2026-04-06 Thời gian đọc: 9 phút

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ị — xác minh một mô-đun duy nhất (hàm, phương thức, lớp) độc lập với các phụ thuộc bên ngoài
  • Nguyên tắc FIRST — Fast, Isolated, Repeatable, Self-validating, Timely — nền tảng của kiểm thử chất lượng
  • Mock và stub — vật thay thế cho các phụ thuộc bên ngoài (DB, API, hệ thống tệp), đảm bảo sự độc lập của kiểm thử
  • TDD (Test-Driven Development) — phương pháp phát triển hướng kiểm thử: đỏ-xanh-tái cấu trúc
  • Kim tự tháp kiểm thử — kiểm thử đơn vị chiếm 70% kim tự tháp, cung cấp phản hồi nhanh ở mỗi commit

Kiểm thử đơn vị là gì?

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.

Tại sao cần kiểm thử đơn vị?

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.

Điều gì được coi là kiểm thử đơn vị?

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ử.

Nguyên tắc FIRST và cấu trúc AAA

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ử.

Cấu trúc AAA (Arrange-Act-Assert)

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.

kotlin
// 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 tên kiểm thử

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.

Mock, stub và fake: dùng cái gì và khi nào

Để 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 saoXác minh gìVí dụ
MockGọi phương thức với tham số đúnguserRepository.save(user) được gọi đúng 1 lần
StubGiá trị trả vềrepository.findById(1) trả về User(id=1, name="Test")
FakeLogic qua triển khai đơn giản hóaInMemoryMapUserRepository với HashMap thay vì DB
SpyMock một phần đối tượng thựcspy(repo).when(findById).thenReturn(user)

Mockito: ví dụ mocking trong Java/Kotlin

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.

kotlin
// 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: phát triển hướng kiểm thử

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.

Lợi ích của TDD

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.

Khi nào TDD không phù hợp?

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.

Kiểm thử đơn vị trong ứng dụng di động

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ị trên Android (JUnit + Mockito/Robolectric)

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ị trên iOS (XCTest + Quick/Nimble)

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ị trên Flutter (flutter_test + Mockito)

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.

dart
// 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);
    });
}

Thực hành tốt nhất và lỗi thường gặp

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.

  • Một xác minh cho mỗi kiểm thử — một assert hoặc một nhóm assert liên quan cho một xác minh logic
  • Tránh trùng lặp — sử dụng @BeforeEach / setUp để khởi tạo chung, kiểm thử tham số hóa cho dữ liệu đầu vào khác nhau
  • Không kiểm tra phương thức riêng — kiểm tra qua API công khai. Nếu phương thức riêng không được bao phủ, logic của nó không hiển thị bên ngoài
  • Bao phủ các trường hợp biên — bộ sưu tập rỗng, null/undefined, số âm, giá trị tối đa
  • Không sử dụng Thread.sleep trong kiểm thử — làm kiểm thử chậm và không ổn định. Sử dụng thời gian chờ kiểm thử và coroutine

Mức độ bao phủ nào được coi là đủ?

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%.

CI/CD và kiểm thử đơn vị

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ị khác kiểm thử tích hợp như thế nào?

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%.

Nên chọn framework nào cho kiểm thử đơn vị?

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.

Các nguyên tắc F.I.R.S.T. trong kiểm thử là gì?

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ó cần viết kiểm thử đơn vị cho ViewModel trên Android/iOS không?

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.

Làm thế nào để kiểm tra mã có yêu cầu mạng?

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

  • Kiểm thử đơn vị — xác minh các mô-đun riêng lẻ độc lập với các phụ thuộc bên ngoài với phản hồi nhanh
  • Cấu trúc AAA — Arrange (chuẩn bị), Act (hành động), Assert (xác minh) — mẫu kiểm thử tiêu chuẩn
  • Mock và stub — bản sao kiểm thử để cô lập: mock xác minh lời gọi, stub trả về giá trị
  • TDD — phát triển hướng kiểm thử (Đỏ-Xanh-Tái cấu trúc) giảm số lượng lỗi 40-80%
  • Nguyên tắc FIRST — Fast, Isolated, Repeatable, Self-validating, Timely — nền tảng kiểm thử chất lượng
  • Công cụ theo nền tảng — JUnit 5 (Android), XCTest (iOS), flutter_test (Flutter), Jest (React Native)
  • Bao phủ 70-80% — mức tối ưu cho logic nghiệp vụ quan trọng, getter và setter không yêu cầu kiểm thử

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.

Thảo luận dự án

Đọc thêm