# Chương 4: Kiểm thử đơn vị (Unit Test)

Chương này hướng dẫn cách viết unit test cho API trong ứng dụng Java Spring Boot. Nội dung bao gồm cấu trúc test chuẩn, cách mock dependency, kiểm tra validate input, xác thực (authentication/authorization), ghi log, xử lý exception và kiểm tra logic service. Mục tiêu đảm bảo mỗi API hoạt động đúng, an toàn và ổn định trước khi tích hợp hoặc triển khai.

# Nội dung chi tiết

## 1. Nguyên tắc Unit Test API

Mỗi API cần test các nhóm case cơ bản:

1. Validate input
2. Auth / permission
3. Business logic
4. Repository interaction
5. Exception handling
6. Logging
7. HTTP status &amp; response

---

# 2. Ví dụ API mẫu

Controller:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40restcontroller-%40req"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@RestController</span><span class="hljs-meta">@RequestMapping("/users")</span><span class="hljs-meta">@RequiredArgsConstructor</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UserController</span> {    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UserService userService;    <span class="hljs-meta">@PostMapping</span>    <span class="hljs-keyword">public</span> ResponseEntity<UserResponse> <span class="hljs-title function_">create</span><span class="hljs-params">(            </span><span class="hljs-meta">@Valid</span> <span class="hljs-meta">@RequestBody</span> CreateUserRequest req) {        <span class="hljs-keyword">return</span> ResponseEntity.ok(userService.create(req));    }}`</div></div>DTO:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40data-public-class-c"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@Data</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">CreateUserRequest</span> {    <span class="hljs-meta">@NotBlank</span>    <span class="hljs-keyword">private</span> String username;    <span class="hljs-meta">@Email</span>    <span class="hljs-keyword">private</span> String email;}`</div></div>Service:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40service-%40requiredar"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@Service</span><span class="hljs-meta">@RequiredArgsConstructor</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UserService</span> {    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UserRepository repo;    <span class="hljs-keyword">public</span> UserResponse <span class="hljs-title function_">create</span><span class="hljs-params">(CreateUserRequest req)</span> {        <span class="hljs-type">User</span> <span class="hljs-variable">u</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(req.getUsername(), req.getEmail());        repo.save(u);        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">UserResponse</span>(u.getId(), u.getUsername());    }}`</div></div>---

# 3. Cấu trúc test chuẩn

Dependency:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%3Cdependency%3E-%3Cgroupi"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-tag"><<span class="hljs-name">dependency</span></span>>  <span class="hljs-tag"><<span class="hljs-name">groupId</span></span>>org.springframework.boot<span class="hljs-tag"></<span class="hljs-name">groupId</span></span>>  <span class="hljs-tag"><<span class="hljs-name">artifactId</span></span>>spring-boot-starter-test<span class="hljs-tag"></<span class="hljs-name">artifactId</span></span>>  <span class="hljs-tag"><<span class="hljs-name">scope</span></span>>test<span class="hljs-tag"></<span class="hljs-name">scope</span></span>><span class="hljs-tag"></<span class="hljs-name">dependency</span></span>>`</div></div>Controller test dùng:

- @WebMvcTest
- MockMvc
- @MockBean

---

# 4. Test Validate Input

Case cần có:

- thiếu field bắt buộc
- email sai format
- request null

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40webmvctest%28usercont"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@WebMvcTest(UserController.class)</span><span class="hljs-keyword">class</span> <span class="hljs-title class_">UserControllerValidationTest</span> {    <span class="hljs-meta">@Autowired</span>    <span class="hljs-keyword">private</span> MockMvc mockMvc;    <span class="hljs-meta">@MockBean</span>    <span class="hljs-keyword">private</span> UserService userService;    <span class="hljs-meta">@Test</span>    <span class="hljs-keyword">void</span> <span class="hljs-title function_">create_shouldFail_whenUsernameBlank</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception {        <span class="hljs-type">String</span> <span class="hljs-variable">json</span> <span class="hljs-operator">=</span> <span class="hljs-string">"""            {"username":"","email":"a@test.com"}        """</span>;        mockMvc.perform(post(<span class="hljs-string">"/users"</span>)                .contentType(MediaType.APPLICATION_JSON)                .content(json))                .andExpect(status().isBadRequest());    }    <span class="hljs-meta">@Test</span>    <span class="hljs-keyword">void</span> <span class="hljs-title function_">create_shouldFail_whenEmailInvalid</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception {        <span class="hljs-type">String</span> <span class="hljs-variable">json</span> <span class="hljs-operator">=</span> <span class="hljs-string">"""            {"username":"john","email":"invalid"}        """</span>;        mockMvc.perform(post(<span class="hljs-string">"/users"</span>)                .contentType(MediaType.APPLICATION_JSON)                .content(json))                .andExpect(status().isBadRequest());    }}`</div></div>---

# 5. Test Auth / Permission

Giả sử API cần login:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40withmockuser-%40test-"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@WithMockUser</span><span class="hljs-meta">@Test</span><span class="hljs-keyword">void</span> <span class="hljs-title function_">create_shouldSuccess_whenAuthenticated</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception {    <span class="hljs-keyword">when</span>(userService.create(any()))        .thenReturn(<span class="hljs-keyword">new</span> <span class="hljs-title class_">UserResponse</span>(<span class="hljs-number">1L</span>,<span class="hljs-string">"john"</span>));    <span class="hljs-type">String</span> <span class="hljs-variable">json</span> <span class="hljs-operator">=</span> <span class="hljs-string">"""        {"username":"john","email":"a@test.com"}    """</span>;    mockMvc.perform(post(<span class="hljs-string">"/users"</span>)            .contentType(MediaType.APPLICATION_JSON)            .content(json))            .andExpect(status().isOk());}`</div></div>Test chưa login:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40test-void-create_sh"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@Test</span><span class="hljs-keyword">void</span> <span class="hljs-title function_">create_shouldUnauthorized_whenNoAuth</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception {    <span class="hljs-type">String</span> <span class="hljs-variable">json</span> <span class="hljs-operator">=</span> <span class="hljs-string">"""        {"username":"john","email":"a@test.com"}    """</span>;    mockMvc.perform(post(<span class="hljs-string">"/users"</span>)            .contentType(MediaType.APPLICATION_JSON)            .content(json))            .andExpect(status().isUnauthorized());}`</div></div>---

# 6. Test Business Logic (Service)

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40extendwith%28mockitoe"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@ExtendWith(MockitoExtension.class)</span><span class="hljs-keyword">class</span> <span class="hljs-title class_">UserServiceTest</span> {    <span class="hljs-meta">@Mock</span>    <span class="hljs-keyword">private</span> UserRepository repo;    <span class="hljs-meta">@InjectMocks</span>    <span class="hljs-keyword">private</span> UserService service;    <span class="hljs-meta">@Test</span>    <span class="hljs-keyword">void</span> <span class="hljs-title function_">create_shouldSaveUser</span><span class="hljs-params">()</span> {        <span class="hljs-type">CreateUserRequest</span> <span class="hljs-variable">req</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">CreateUserRequest</span>();        req.setUsername(<span class="hljs-string">"john"</span>);        req.setEmail(<span class="hljs-string">"a@test.com"</span>);        <span class="hljs-keyword">when</span>(repo.save(any())).thenAnswer(i -> i.getArgument(<span class="hljs-number">0</span>));        <span class="hljs-type">UserResponse</span> <span class="hljs-variable">res</span> <span class="hljs-operator">=</span> service.create(req);        assertEquals(<span class="hljs-string">"john"</span>, res.getUsername());        verify(repo).save(any());    }}`</div></div>---

# 7. Test Exception Handling

Giả sử email trùng:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-when%28repo.save%28any%28%29"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-keyword">when</span>(repo.save(any()))    .thenThrow(<span class="hljs-keyword">new</span> <span class="hljs-title class_">DataIntegrityViolationException</span>(<span class="hljs-string">"dup"</span>));`</div></div>Test:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40test-void-create_sh-1"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@Test</span><span class="hljs-keyword">void</span> <span class="hljs-title function_">create_shouldThrow_whenDuplicateEmail</span><span class="hljs-params">()</span> {    <span class="hljs-type">CreateUserRequest</span> <span class="hljs-variable">req</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">CreateUserRequest</span>();    req.setUsername(<span class="hljs-string">"john"</span>);    req.setEmail(<span class="hljs-string">"a@test.com"</span>);    <span class="hljs-keyword">when</span>(repo.save(any()))        .thenThrow(<span class="hljs-keyword">new</span> <span class="hljs-title class_">RuntimeException</span>(<span class="hljs-string">"duplicate"</span>));    assertThrows(RuntimeException.class,        () -> service.create(req));}`</div></div>---

# 8. Test Logging

Giả sử service có log:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-log.info%28%22create-use"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`log.info(<span class="hljs-string">"Create user {}"</span>, req.getUsername());`</div></div>Test log:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40extendwith%28mockitoe-1"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@ExtendWith(MockitoExtension.class)</span><span class="hljs-keyword">class</span> <span class="hljs-title class_">UserServiceLogTest</span> {    <span class="hljs-meta">@Mock</span>    <span class="hljs-keyword">private</span> UserRepository repo;    <span class="hljs-meta">@InjectMocks</span>    <span class="hljs-keyword">private</span> UserService service;    <span class="hljs-meta">@Test</span>    <span class="hljs-keyword">void</span> <span class="hljs-title function_">create_shouldLog</span><span class="hljs-params">()</span> {        <span class="hljs-type">CreateUserRequest</span> <span class="hljs-variable">req</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">CreateUserRequest</span>();        req.setUsername(<span class="hljs-string">"john"</span>);        req.setEmail(<span class="hljs-string">"a@test.com"</span>);        <span class="hljs-keyword">when</span>(repo.save(any())).thenAnswer(i -> i.getArgument(<span class="hljs-number">0</span>));        service.create(req);        verify(repo).save(any());    }}`</div></div>(Thực tế có thể dùng LogCaptor)

---

# 9. Test HTTP Response

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%40test-%40withmockuser-"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-meta">@Test</span><span class="hljs-meta">@WithMockUser</span><span class="hljs-keyword">void</span> <span class="hljs-title function_">create_shouldReturnBody</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception {    <span class="hljs-keyword">when</span>(userService.create(any()))        .thenReturn(<span class="hljs-keyword">new</span> <span class="hljs-title class_">UserResponse</span>(<span class="hljs-number">1L</span>,<span class="hljs-string">"john"</span>));    <span class="hljs-type">String</span> <span class="hljs-variable">json</span> <span class="hljs-operator">=</span> <span class="hljs-string">"""        {"username":"john","email":"a@test.com"}    """</span>;    mockMvc.perform(post(<span class="hljs-string">"/users"</span>)            .contentType(MediaType.APPLICATION_JSON)            .content(json))            .andExpect(status().isOk())            .andExpect(jsonPath(<span class="hljs-string">"$.username"</span>).value(<span class="hljs-string">"john"</span>));}`</div></div>---

# 10. Danh sách Case chuẩn cho mỗi API

Mỗi API cần tối thiểu:

### Validate

- [ ]  [ ]  thiếu field
- [ ]  [ ]  format sai
- [ ]  [ ]  null body

### Auth

- [ ]  [ ]  chưa login
- [ ]  [ ]  sai role
- [ ]  [ ]  đúng role

### Business

- [ ]  [ ]  flow thành công
- [ ]  [ ]  dữ liệu biên
- [ ]  [ ]  điều kiện đặc biệt

### Repository

- [ ]  [ ]  save OK
- [ ]  [ ]  not found
- [ ]  [ ]  duplicate

### Exception

- [ ]  [ ]  DB lỗi
- [ ]  [ ]  logic lỗi
- [ ]  [ ]  external lỗi

### Response

- [ ]  [ ]  status code
- [ ]  [ ]  body
- [ ]  [ ]  schema

### Log

- [ ]  [ ]  log khi success
- [ ]  [ ]  log khi error

---

# 11. Checklist Unit Test API

- [ ]  [ ]  Test validate
- [ ]  [ ]  Test auth
- [ ]  [ ]  Test service
- [ ]  [ ]  Mock repo
- [ ]  [ ]  Test exception
- [ ]  [ ]  Test response
- [ ]  [ ]  Verify interaction
- [ ]  [ ]  No DB thật
- [ ]  [ ]  No network