์ด๋ฒ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ์ฒ์์ผ๋ก ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณด์๋ค.
๊ทธ ์ ์๋ ์ธ๊ฐ ๊ฐ์ฌ๋์ด ์น๋ ๊ฒ๋ง ๋ฐ๋ผ์ณ๋ดค๋๋ฐ, ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ค๋ ์คํ๋ง๋ถํธ๊ฐ ๋จ๋ ๋ฐ ๋๋ฌด ์ค๋ ๊ฑธ๋ ธ๋ค๐ฅฒ
์ ํํ ์๊ฐ์ ์ฌ๋ณด์ง ์์์ง๋ง ๋๋ต 3~5์ด์ ์๊ฐ์ด ๊ฑธ๋ ธ๊ณ , ์ด์ ๋ํ ํด๊ฒฐ๋ฐฉ๋ฒ์ ์ฐพ๋ค๊ฐ Mockito๋ผ๋ ๊ฑธ ์ฌ์ฉํด๋ณด๊ฒ ๋์๋ค.
1. Mockito๋?
Mockito๋ Mock, ์ฝ๊ฒ ๋งํด ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ์ง์ํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ฉด ๋จ์ ํ
์คํธ๊ฐ ํธํด์ง๋ค.
๊ธฐ์กด์ ํตํฉ ํ
์คํธ๋ ๋ชจ๋ ๋น์ ๋์ฐ๊ธฐ ๋๋ฌธ์ ์คํํ๋ ๋ฐ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฐ๋ค. ๋ํ ๋ค๋ฅธ ํด๋์ค์ ์ ์ ๋์ ์ฌ๋ถ์ ๋ฐ๋ผ ํ
์คํธ ๊ฒฐ๊ณผ๊ฐ ๋ฐ๋๋ค๋ ๋ฌธ์ ์ ์ด ์์๋ค. Service์ createTag()๊ฐ ์ ์ ๋์ํ๋ ๊ฑธ ํ์ธํ๊ณ ์ถ์ด๋, Repository์ ๋ฌธ์ ๊ฐ ์์ผ๋ฉด createTagTest() ๋ฉ์๋ ์์ฒด๊ฐ ์คํจํ๋ ์์ด๋ค.
๊ฐ๋ฐ์๊ฐ ์ง์ ๊ฐ์ง ๊ฐ์ฒด ํด๋์ค๋ฅผ ์์ฑํ์ฌ ํ
์คํธํ๋ ๋ฐฉ๋ฒ๋ ์์ง๋ง, ํ
์คํธ์ฉ ๊ฐ์ง ๊ฐ์ฒด ํด๋์ค๋ฅผ ๊ด๋ฆฌํ๋ ๊ฑด ๋ฒ๊ฑฐ๋กญ๊ณ ๊ท์ฐฎ๋ค.
์๋ง ๊ท์ฐฎ์์ ์ด๊ธฐ์ง ๋ชปํ ๋๊ตฐ๊ฐ๊ฐ ์ด๋ ๊ฒ ์ข์๊ฑธ ๋ง๋ค์ด์ค๊ฒ ์๋๊น~?๐คทโ๏ธ
์ด๋ฌํ ์ด์ ๋ก Mockito๋ฅผ ์ด์ฉํด ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ, ๋จ์ ํ
์คํธ์ ๋์ ํด๋ณด๊ธฐ๋ก ํ๋ค๐ฅ
2. ์ปจํธ๋กค๋ฌ ๋จ์ ํ
์คํธ
@RestController
@RequestMapping("/api/v1/tags")
@RequiredArgsConstructor
public class TagController {
private final Long TEMP_USER_ID = 1L;
private final TagService tagService;
@PostMapping
public ResponseEntity<Void> createTag(@RequestBody @Valid final TagCreateRequest request) {
tagService.save(TEMP_USER_ID, request);
return ResponseEntity.noContent().build();
}
}
"/api/v1/tags"๋ผ๋ Path๋ก POST ์์ฒญ์ ๋ณด๋ด, ์๋ต์ฝ๋ 204๊ฐ ์ ๋๋ก ์ค๋์ง๋ฅผ ํ ์คํธํด๋ณด์.
(1) @WebMvcTest๋ฅผ ํตํด ์น ๊ณ์ธต ํ ์คํธ ์ค๋น
@WebMvcTest(TagController.class)
class TagControllerTest {
...
}
- @WebMvcTest: SpringMVC ์ปดํฌ๋ํธ์๋ง ์ง์คํ ํ ์คํธ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํด ์ฃผ๋ ์ ๋ ธํ ์ด์ ์ด๋ค.
@WebMvcTest๋ฅผ ์ฌ์ฉํ๋ฉด, ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ฒด ์ปจํ ์คํธ๊ฐ ์๋ TagController์ ๊ด๋ จ๋ ์น ๊ณ์ธต ๋น๋ค๋ง ๋ก๋ํ ์ ์๋ค. (๋ฐ๋ผ์ Controller, Filter, JsonComponent ๋ฑ์ ๋ก๋๋์ง๋ง, Service, Repository ๋ฑ์ ๋ก๋๋์ง ์๋๋ค.)
์ฐธ๊ณ ๋ก @WebMvcTest๊ฐ ๋ถ์ ํ ์คํธ๋ MockMvc์ ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ์๋์ผ๋ก ์ค์ ๋์ด ์๋ค.
(2) ํ์ํ ๊ฐ์ฒด ์์ฑ ๋ฐ ์ฃผ์ ํ๊ธฐ
@WebMvcTest(TagController.class)
class TagControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private TagService tagService;
}
- MockMvc: ์๋ฒ ์ฌ์ด๋ Spring MVC ํ ์คํธ๋ฅผ ์ง์ํ๋ ํด๋์ค๋ก, HTTP ์์ฒญ ๋ฐ ์๋ต์ ์๋ฎฌ๋ ์ด์ ์ ๋ด๋นํ๋ค.
- ObjectMapper: ๊ฐ์ฒด ⇔ JSON ์ฌ์ด์ ๋ณํ์ ๋ด๋นํ๋ค. ์ฌ๊ธฐ์๋ RequestDTO๋ฅผ JSON์ผ๋ก ๋ณํํ์ฌ HTTP Request Body์ ๋ฃ๊ธฐ ์ํด ์ฌ์ฉํ์๋ค.
- @MockBean: ๊ฐ์ง ๋น ๊ฐ์ฒด๋ฅผ ์์ฑํด์ค๋ค. ์ฌ๊ธฐ์๋ @WebMvcTest์ ์ํด ์คํ๋ง ์ปจํ ์คํธ์ ์ผ๋ถ๊ฐ ๋ก๋๋์์ผ๋ฏ๋ก, @Mock ๋์ @MockBean์ ์ฌ์ฉํด์ฃผ์ด์ผ ํ๋ค.
(3) ํ ์คํธ ๋ฉ์๋ ์์ฑ
@Test
void ํ๊ทธ_๋ฑ๋ก_์์ฒญ์_์ฑ๊ณตํ๋ค() throws Exception {
// given
final TagCreateRequest tagCreateRequest = new TagCreateRequest("tag_Name");
// when
final ResultActions resultActions = mockMvc.perform(post("/api/v1/tags")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(tagCreateRequest)));
// then
resultActions.andExpect(status().isNoContent());
}
๋จผ์ ์์ฒญ์ ์ด์ฉํ RequestDTO๋ฅผ ์์ฑํ ๋ค์, MockMvc.perform()์ ์ด์ฉํด HTTP Request๋ฅผ ๋ณด๋ธ๋ค.
perform()์ HTTP ์์ฒญ์ ์๋ฎฌ๋ ์ดํธํ๋ ๋ฐ ์ฌ์ฉ๋๋ ๋ฉ์๋๋ก, ์ฒด์ด๋์ ํตํด GET, POST ๋ฑ์ HTTP Method์ ์์ฒญ ํค๋, ์ฟ ํค ๊ฐ์ ์ง์ ํ ์ ์๋ค. (Content-Type ํค๋๊ฐ application/json์์ ์ฃผ์ํ์.)
ObjectMapper๋ฅผ ์ด์ฉํด RequestDTO ๊ฐ์ฒด๋ฅผ JSON์ผ๋ก ๋ณํํ์ฌ Request Body์ ๋ด์ ์์ฒญ์ ๋ณด๋ด๋ฉด, ๊ทธ์ ๋ํ ์๋ต์ ResultAction์ ๋ด๊ธด๋ค.
ResultAction์ HTTP ์๋ต์ ๊ฒ์ฆํ๋๋ฐ ์ฌ์ฉ๋๋ ํด๋์ค๋ก, ์๋์ ๊ฐ์ ๋ฉ์๋๋ฅผ ์ง์ํ๋ค.
- andExpect(): ์๋ต ์ฝ๋, ์๋ต ๋ฐ๋์ ๋ด๊ธด ๋ด์ฉ ๋ฑ์ ๊ฒ์ฆํ ์ ์๋ค. ๋ง์ฝ ๊ธฐ๋๊ฐ๊ณผ ์ค์ ๊ฒฐ๊ณผ๊ฐ์ด ๋ค๋ฅผ ๊ฒฝ์ฐ ์์ธ๊ฐ ๋ฐ์ํ๋ค.
- andDo(): ์๋ต ๋ด์ฉ์ ์ถ๋ ฅํ๊ฑฐ๋ ๋ก๊ทธ์ ๊ธฐ๋กํ ๋ ์ฌ์ฉํ๋ค.
- andReturn(): ์๋ต ๊ฒฐ๊ณผ๋ฅผ MvcResult ๋ผ๋ ๊ฐ์ฒด์ ๋ด์ ๋ฐํํ๋ค.
์ฌ๊ธฐ์๋ ์๋ต์ฝ๋ 204๋ฅผ ๋ฐํ์ ๊ธฐ๋ํ๋๋ก ์ค์ ํ๋ค.
(4) ์คํ ๊ฒฐ๊ณผ ํ์ธ
3. ์๋น์ค ๋จ์ ํ ์คํธ
@Service
@RequiredArgsConstructor
@Transactional
public class TagService {
private final TagRepository tagRepository;
private final UserRepository userRepository;
public Long save(final Long userId, final TagCreateRequest tagCreateRequest) {
final User user = userRepository.findById(userId)
.orElseThrow(() -> new BadRequestException(NOT_FOUND_USER));
if (checkDuplicateTagName(user.getId(), tagCreateRequest.getName())) {
throw new BadRequestException(DUPLICATED_TAG_NAME);
}
final Tag createdTag = Tag.builder()
.name(tagCreateRequest.getName())
.user(user)
.createdAt(LocalDateTime.now())
.build();
Tag tag = tagRepository.save(createdTag);
return tag.getId();
}
}
์ด์ TagService์ ๋ํ ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํด ๋ณด์. TagService๊ฐ save()ํจ์๋ฅผ ํธ์ถํ์ ๋, ์ ์ฅ๋ ํ๊ทธ์ id๋ฅผ ์ ๋๋ก ๋ฐํํ๋์ง ํ์ธํด๋ณด๋ ค๊ณ ํ๋ค.
TagService๋ ์คํ๋ง์ผ๋ก๋ถํฐ TagRepository์ UserRepository๋ฅผ ์ฃผ์
๋ฐ๊ณ ์๋ค.
(1) Junit๊ณผ Mockito ๊ฒฐํฉํ๊ธฐ
@ExtendWith(MockitoExtension.class)
class TagServiceTest {
...
}
๋จผ์ Mockito์ Junit์ ํจ๊ป ์ฌ์ฉํ๊ธฐ ์ํด์ ํ
์คํธ ํด๋์ค ์์ @ExtendWith(MockitoExtension.class) ์ ๋
ธํ
์ด์
์ ๋ฌ์์ค๋ค.
(2) ๊ฐ์ง ๊ฐ์ฒด ์์ฑ ๋ฐ ์ฃผ์ ํ๊ธฐ
@ExtendWith(MockitoExtension.class)
class TagServiceTest {
@InjectMocks
private TagService tagService;
@Mock
private TagRepository tagRepository;
@Mock
private UserRepository userRepository;
}
์ด๋ ๊ฒ ํ
์คํธ์ ํ์ํ ๊ฐ์ฒด๋ค์ ์ ์ธํด์ค๋ค.
- @InjectMocks: ํ
์คํธํ ๊ฐ์ฒด๊ฐ ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ์ฃผ์
๋ฐ์ ์ ์๋๋ก ํด์ค๋ค. ์ฌ๊ธฐ์๋ tagService์ ๋ถ์ฌ์ฃผ์๋ค.
- @Mock: ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ์์ฑํด์ฃผ๋ ์ ๋
ธํ
์ด์
์ด๋ค. ์ด์ tagService๋ ๊ฐ์ง tagRepository์ ๊ฐ์ง userRepository๋ฅผ ์ฃผ์
๋ฐ์ ์ ์๋ค.
(3) ํ ์คํธ ๋ฉ์๋ ์์ฑ
tagService๊ฐ ์ค์ ๋ก save() ๋ฉ์๋๋ฅผ ํธ์ถ๋ฐ์์ ๋, ์ ์ฅํ ํ๊ทธ์ id๋ฅผ ์ ๋ฆฌํดํ๋์ง๋ฅผ ํ
์คํธํด๋ณด์.
static Tag COMPANY_TAG = Tag.builder()
.id(1L)
.name("ํ์ฌ")
.createdAt(LocalDateTime.now())
.build();
static User USER_KIM = User.builder()
.id(2L)
.email("siltarae@nn.com")
.createdAt(LocalDateTime.now())
.nickname("Kim")
.socialType(SocialType.GOOGLE)
.build();
๋จผ์ ํ
์คํธ์ฉ ๋ฐ์ดํฐ๋ฅผ ์ค๋นํ๋ค.
์ฌ๊ธฐ์๋ ํ์ฌ ํ๊ทธ์, ์ด๋ฆ์ด Kim์ธ ์ฌ์ฉ์๋ฅผ ์ค๋นํ๋ค.
@Test
void ํ๊ทธ_์์ฑ_ํ_tagId๋ฅผ_๋ฐํํ๋ค() {
// given
final TagCreateRequest tagCreateRequest = new TagCreateRequest(COMPANY_TAG.getName());
given(userRepository.findById(2L))
.willReturn(Optional.ofNullable(USER_KIM));
given(tagRepository.save(any(Tag.class)))
.willReturn(COMPANY_TAG);
// when
final Long actualId = tagService.save(2L, tagCreateRequest);
// then
assertThat(actualId).isEqualTo(1L);
}
์ด์ stubbing์ ํ ์ฐจ๋ก์ด๋ค. stubbing์ด๋, ๊ฐ์ง ๊ฐ์ฒด์ ํน์ ํ๋์ ์ง์ ํ๋ ๊ฒ์ธ๋ฐ, ํ
์คํธ ํ๊ฒฝ์ ํ์คํ ์ ์ดํ๊ธฐ ์ํด ์ฌ์ฉํ๋ค.
๋ง์ฝ userRepository๊ฐ ์๋ฑํ๊ฒ ํ๋ํ๋ฉด? ์ ํ
์คํธ๋ ์คํจํ๋ค. ๊ทธ๋์ ๋ฏธ๋ฆฌ ๊ฐ์ง ๊ฐ์ฒดํํ
'๋ ์ด ๋ฉ์๋ ํธ์ถ๋๋ฉด ์ด๋ ๊ฒ ํ๋ํด์ผํด. ์์์ง?' ๋ผ๊ณ ์ผ๋ฌ๋๋ ๊ฒ์ด๋ค. ์ฌ๊ธฐ์๋ BDD ๊ตฌ์กฐ์ ๋ง์ถ์ด ์์ฑํ ์ ์๋๋ก BDDMockito๋ฅผ ์ฌ์ฉํ๋ค.
given()์ ์ด๋ค ๊ฐ์ฒด๊ฐ ์ด๋ค ๋ช
๋ น์ ๋ฐ์์ ๋๋ฅผ ์ง์ ํ๋ค. ์์์ userRepository๋ id=2์ธ User๋ฅผ ์ฐพ์ผ๋ผ๋ ๋ช
๋ น์ ๋ฐ์์ ๋๋ฅผ ์ง์ ๋ฐ์๋ค.
๊ทธ๋ฆฌ๊ณ willReturn()์ ํตํด ํด๋น ์ํฉ์์ ์ด๋ค ๊ฐ์ ๋ฐํํ ์ง๋ฅผ ์ค์ ํ ์ ์๋ค. userRepository๋ ํด๋น ์ํฉ์์ ํญ์ USER_KIM์ ๋ฐํํ๋ค.
๐ก BDDMockto๋?
Mockito๋ฅผ BDD ๊ตฌ์กฐ์ ๋ง์ถฐ ์ฌ์ฉํ ์ ์๋๋ก ํ์ฅํ ๊ฒ. ๊ธฐ๋ฅ์ ์ฐจ์ด๋ ์์ง๋ง, ๊ฐ๋ ์ฑ์ด ํฅ์๋ ๋ฉ์๋๋ช ์ ์ ๊ณตํ๋ค.
when ์ ์์๋ ์ค์ tagService์ save() ๋ฉ์๋๋ฅผ ํธ์ถํ ๋ค, ๊ฒฐ๊ณผ๋ก ๋ฐํ๋ ๊ฐ์ ๋ฐ์์จ๋ค.
๋ง์ง๋ง์ผ๋ก then ์ ์์ ๋ฐํ๊ฐ์ด 1์ด ๋ง๋์ง๋ฅผ ์ฒดํฌํ๋ฉด ํ
์คํธ๊ฐ ๋๋๋ค.
(4) ์คํ ๊ฒฐ๊ณผ ํ์ธ