Spring Boot bài 25: Unit test Service với JUnit và Mockito
- 18-10-2026
- Toanngo92
- 0 Comments
Giáo trình Spring Boot MVC và REST API · Chương 11/12: Kiểm thử Service, MVC và API · Bài 25/28 · Biên soạn tháng 10/2026.
Mục lục
Mục tiêu bài học
Kiểm tra quy tắc Service bằng mock, chạy nhanh không cần Spring context và chọn hành vi đáng kiểm tra.
Unit test trả lời câu hỏi nhỏ
CourseService cần Repository, nhưng một test quy tắc “không tìm thấy khóa học phải báo lỗi” không cần khởi động web server hoặc MySQL. Mockito tạo đối tượng thay thế cho repository, còn test quyết định dữ liệu trả về. Nhờ constructor injection, Service có thể được tạo và gọi như một class Java bình thường.
JUnit cung cấp vòng đời và assertion/test engine; Mockito hỗ trợ mock; AssertJ giúp viết kỳ vọng dễ đọc. Bộ dependency test theo Spring Boot quản lý các phiên bản tương thích. Không áp đặt một số phiên bản JUnit từ bài cũ vào pom chỉ vì package org.junit.jupiter vẫn có tên quen thuộc.
Tệp trong mã nguồn mẫu: src/test/java/com/hocvietcode/academy/CourseServiceTest.java. Đoạn dưới được trích từ tệp này; xem bản tải về để có đủ import và phần còn lại.
package com.hocvietcode.academy;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Optional;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class CourseServiceTest {
@Mock CourseRepository courses; @Mock CategoryRepository categories;
@InjectMocks CourseService service;
@Test void missingCourseHasDomainError(){
when(courses.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(()->service.get(99L)).isInstanceOf(CourseNotFound.class);
}
@Test void invalidPageDoesNotQueryDatabase(){
assertThatThrownBy(()->service.search("",0,51,"id")).isInstanceOf(IllegalArgumentException.class);
verifyNoInteractions(courses);
}
@Test void updateChangesManagedEntity(){
Category c=new Category("Java"); Course existing=new Course("Old",BigDecimal.ZERO,c);
when(courses.findById(1L)).thenReturn(Optional.of(existing));when(categories.findById(1L)).thenReturn(Optional.of(c));
CourseForm f=new CourseForm();f.setTitle("New title");f.setPrice(new BigDecimal("100.00"));f.setCategoryId(1L);
assertThat(service.update(1L,f).title()).isEqualTo("New title");
assertThat(existing.getPrice()).isEqualByComparingTo("100.00");
}
}
Arrange, Act, Assert
Arrange chuẩn bị đầu vào và hành vi mock; Act gọi phương thức cần kiểm tra; Assert xác nhận kết quả hoặc lỗi. Test missingCourseHasDomainError thiết lập repository không có id rồi mong CourseNotFound. Test invalidPageDoesNotQueryDatabase kiểm tra đầu vào size vượt giới hạn bị từ chối trước truy vấn.
Test updateChangesManagedEntity xác nhận các giá trị nghiệp vụ được đổi đúng. Nó không cố chứng minh Hibernate dirty checking hoạt động, vì ở đây repository là mock và không có persistence context thật. Phần đó thuộc kiểm thử tích hợp với database.
Mock vừa đủ để không khóa chặt implementation
Không cần verify mọi dòng getter/setter hoặc mọi lời gọi nội bộ chỉ vì có thể. Kiểm tra hành vi quan trọng như từ chối đầu vào, lỗi tài nguyên thiếu và kết quả cập nhật có giá trị hơn việc test một phương thức chỉ để đếm số lần nó được gọi.
Mock không thay cho ràng buộc DB, SQL hay transaction. Một test pass với thenReturn có thể che tên truy vấn sai nếu không có test tích hợp bổ sung. Mỗi tầng test nên có mục đích rõ ràng: unit cho quy tắc cô lập, integration cho cách các thành phần kết nối.
Chạy và đọc báo cáo
Tại thư mục chứa pom.xml, dùng lệnh dưới. Surefire ghi báo cáo trong target/surefire-reports; khi lỗi, tìm test đầu tiên thất bại và so sánh expected với actual trước khi sửa code. Không xóa assertion để làm màu xanh nếu chưa giải thích được thay đổi hành vi.
.\mvnw.cmd test
.\mvnw.cmd -Dtest=CourseServiceTest test
Các test dùng dữ liệu riêng, không kết nối DB production và không gọi Internet hay SMTP thật. Cảnh báo cơ chế gắn agent của Mockito có thể xuất hiện tùy JDK; cần phân biệt cảnh báo với test failure. Nếu thay đổi JDK hoặc Mockito, đọc hướng dẫn chính thức về cấu hình agent và tương thích thay vì thêm cờ JVM không rõ nguồn.
Lỗi thường gặp và lưu ý
- Không dùng @SpringBootTest cho mọi unit test nếu bài toán chỉ cần tạo một Service.
- Một mock test pass không chứng minh migration, quyền truy cập hay serialization đều đúng.
Ghi nhớ
Unit test nhanh và có ý nghĩa khi nó kiểm tra một hành vi cụ thể với đầu vào và kỳ vọng rõ ràng.

