Mockito를 사용한 스프링 MockMvc 테스트
MockMvc란?
MockMvc
는 Spring MVC 컨트롤러 테스트를 도와주는 강력한 도구이다. 내부적으로 HTTP 요청을 모의(mock)하여 컨트롤러, 필터, 인터셉터 등을 통과하게 하고 실제 응답을 생성한다. 이를 통해 전체 애플리케이션을 실행하지 않고도 웹 계층 테스트가 가능해진다.
MockMvc
는 컨트롤러의 요청 파라미터들이 올바르게 매핑되고, 요청과 응답이 예상대로 처리되는지 확인하는 용도로 주로 사용된다.
MockMvc
를 활용한 테스트의 특징은 다음과 같다.
- 빠른 테스트: 실제 서버를 띄우지 않기 때문에 빠른 속도로 테스트를 수행할 수 있다.
- 독립적인 테스트: 외부 종속성을 사용할 필요 없이 단위 테스트에 가깝게 컨트롤러를 테스트할 수 있다.
스프링부트 환경에서 아래 스프링부트 테스트 의존성을 주입하면 MockMvc
를 사용할 수 있다.
1
testImplementation 'org.springframework.boot:spring-boot-starter-test'
컨트롤러 테스트
간단한 User 컨트롤러를 작성하여 테스트해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
@PatchMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
User updatedUser = userService.updateUser(id, updates);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
MockMvc
를 사용하면 HTTP 요청과 응답을 모킹(mock)하여 실제 서블릿 컨테이너 없이도 웹 계층을 테스트할 수 있게 된다.
아래에서는 Mockito
프레임워크를 활용하여 mockMvc 테스트를 위한 가짜 객체를 생성하여 테스트한다.
Mockito
: 실제 객체 대신 가짜 객체를 생성하여 테스트할 수 있게 도와주는 오픈 소스 모킹 프레임워크. 객체 간의 의존성을 모킹하여 특정 메서드 호출에 대한 동작을 미리 정의하고, 테스트 코드 내에서 그 동작이 잘 실행되는지 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import ...
@ExtendWith(MockitoExtension.class)
public class UserControllerTest {
private MockMvc mockMvc;
@Mock
private UserService userService; // UserService를 모킹
@InjectMocks
private UserController userController; // 모킹된 userService가 주입된 UserController
private final ObjectMapper objectMapper = new ObjectMapper();
// 컨트롤러 테스트 설정
public UserControllerTest() {
this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
// GET 요청 테스트
@Test
void shouldReturnUserById() throws Exception {
User mockUser = new User(1L, "John", "Doe");
when(userService.getUserById(1L)).thenReturn(mockUser);
mockMvc.perform(get("/api/user/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.firstName").value("John"))
.andExpect(jsonPath("$.lastName").value("Doe"));
verify(userService, times(1)).getUserById(1L);
}
// POST 요청 테스트
@Test
void shouldCreateUser() throws Exception {
User newUser = new User(null, "Jane", "Doe");
User savedUser = new User(2L, "Jane", "Doe");
when(userService.createUser(any(User.class))).thenReturn(savedUser);
String userJson = objectMapper.writeValueAsString(newUser);
mockMvc.perform(post("/api/user")
.contentType(MediaType.APPLICATION_JSON)
.content(userJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(2L))
.andExpect(jsonPath("$.firstName").value("Jane"))
.andExpect(jsonPath("$.lastName").value("Doe"));
verify(userService, times(1)).createUser(any(User.class));
}
// PATCH 요청 테스트
@Test
void shouldUpdateUser() throws Exception {
User updatedUser = new User(1L, "John", "Smith");
when(userService.updateUser(eq(1L), anyMap())).thenReturn(updatedUser);
Map<String, Object> updates = new HashMap<>();
updates.put("lastName", "Smith");
String patchJson = objectMapper.writeValueAsString(updates);
mockMvc.perform(patch("/api/user/1")
.contentType(MediaType.APPLICATION_JSON)
.content(patchJson))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.firstName").value("John"))
.andExpect(jsonPath("$.lastName").value("Smith"));
verify(userService, times(1)).updateUser(eq(1L), anyMap());
}
// DELETE 요청 테스트
@Test
void shouldDeleteUser() throws Exception {
doNothing().when(userService).deleteUser(1L);
mockMvc.perform(delete("/api/user/1"))
.andExpect(status().isNoContent());
verify(userService, times(1)).deleteUser(1L);
}
}
위의 코드처럼 mockMvc에 가짜 객체를 미리 설정하고, 테스트코드를 통해 검증할 수 있다.
- @ExtendWith(MockitoExtension.class): JUnit 5에서 Mockito를 확장하여 사용할 수 있도록 설정하는 어노테이션. @Mock과 @InjectMocks을 활용할 수 있게된다.
- MockMvcBuilders.standaloneSetup(): MockMvc를 설정하는 방법 중 하나로, 컨트롤러 단위로 테스트할 때 사용한다.
- @Mock, @InjectMocks: UserService를 모킹하고, 모킹된 UserService를 UserController에 주입해주는 어노테이션.
- when(): Mock 객체의 메서드가 호출되었을 때 미리 응답할 return 값을 설정한다.
- mockMvc.perform(): 컨트롤러 매핑 URL로 요청을 모킹한다.
- andExpect(): 요청에 대한 예상 응답 및 상태를 확인한다.
- verify(): 테스트 중에 특정 메서드가 예상대로 호출되었는지 검증한다.
이처럼 컨트롤러 인터페이스 API를 서블릿 컨테이너를 띄우지 않고 간단하게 요청 및 응답을 테스트 해볼 수 있다.
중요한건 직접 서버를 띄우거나 스프링 컨테이너를 생성하지 않고, 가짜 객체를 통해 간단하고 빠르게 테스트 해볼 수 있다는 점이다.
DB까지 접근하지 않고 예상하는 서비스 return 값을 미리 설정해두어 응답하기 때문에 속도가 빠르다.
주로 API 명세서를 빠르게 작성하기 위해 컨트롤러 단위테스트를 진행하거나, DB 접근 없이 End-to-End 테스트가 필요할 때 용이하게 사용할 수 있다.