Spring 프로젝트에서 테스트를 작성할 때 단위 테스트를 위한 Mockito를 사용해보자.
Mockito를 사용하면 좋은 점은 다음과 같다.
- 빠르고 독립적이다.
- 외부 서비스나 데이터베이스와의 상호작용을 실제로 하고 싶지 않을 때 유용하다.
왜 빠르냐, 스프링이 띄워질 때 처리되는 여러 자동화된 작업들이 필요없으므로 @SpringBootTest 애노테이션을 사용한 스프링을 띄운 테스트보다 훨씬 빠르다.
그래서 일단 간단하게 테스트 코드를 만들기 위한 필요 코드들을 살펴보자.
DMakerService
@Slf4j
@Service
@RequiredArgsConstructor
public class DMakerService {
/**
* The repository for accessing and manipulating {@link Developer} data.
*/
private final DeveloperRepository developerRepository;
private final RetiredDeveloperRepository retiredDeveloperRepository;
/**
* Creates a new developer based on the provided {@link CreateDeveloperRequestDTO} payload.
* It first validates the payload and then saves the developer to the database.
*
* @param createDeveloperRequestDTO The payload containing the developer's information.
* @throws DMakerException If the payload validation fails or if a developer with the same memberId already exists.
*/
@Transactional
public CreateDeveloperResponseDTO createDeveloper(CreateDeveloperRequestDTO createDeveloperRequestDTO) {
validationCreatePayload(createDeveloperRequestDTO);
Developer developer = Developer.builder()
.developerLevel(createDeveloperRequestDTO.getDeveloperLevel())
.developerType(createDeveloperRequestDTO.getDeveloperType())
.name(createDeveloperRequestDTO.getName())
.age(createDeveloperRequestDTO.getAge())
.experienceYears(createDeveloperRequestDTO.getExperienceYears())
.memberId(createDeveloperRequestDTO.getMemberId())
.statusCode(EMPLOYED)
.build();
developerRepository.save(developer);
return CreateDeveloperResponseDTO.fromEntity(developer);
}
public List<DeveloperDTO> getAllEmployedDevelopers() {
return developerRepository.findDevelopersByStatusCodeEquals(EMPLOYED).stream()
.map(DeveloperDTO::fromEntity)
.collect(Collectors.toList());
}
public DeveloperDetailDTO getDeveloperByMemberId(String memberId) {
return developerRepository.findByMemberId(memberId)
.map(DeveloperDetailDTO::fromEntity)
.orElseThrow(() -> new DMakerException(NO_DEVELOPER));
}
public DeveloperDetailDTO updateDeveloperByMemberId(String memberId, UpdateDeveloperRequestDTO updateDeveloperRequestDTO) {
Developer findDeveloper = developerRepository.findByMemberId(memberId)
.orElseThrow(() -> new DMakerException(NO_DEVELOPER));
validationUpdatePayload(updateDeveloperRequestDTO, findDeveloper);
findDeveloper.changeDeveloperData(updateDeveloperRequestDTO);
return DeveloperDetailDTO.fromEntity(findDeveloper);
}
@Transactional
public DeveloperDetailDTO deleteDeveloperByMemberId(String memberId) {
Developer findDeveloper = developerRepository.findByMemberId(memberId)
.orElseThrow(() -> new DMakerException(NO_DEVELOPER));
findDeveloper.changeStatusCode(RETIRED);
RetiredDeveloper retiredDeveloper = RetiredDeveloper.builder()
.memberId(findDeveloper.getMemberId())
.age(findDeveloper.getAge())
.name(findDeveloper.getName())
.build();
retiredDeveloperRepository.save(retiredDeveloper);
return DeveloperDetailDTO.fromEntity(findDeveloper);
}
private void validationUpdatePayload(UpdateDeveloperRequestDTO updateDeveloperRequestDTO, Developer updateDeveloper) {
DeveloperLevel developerLevel = updateDeveloperRequestDTO.getDeveloperLevel() != null ? updateDeveloperRequestDTO.getDeveloperLevel() : updateDeveloper.getDeveloperLevel();
Integer experienceYears = updateDeveloperRequestDTO.getExperienceYears() != null ? updateDeveloperRequestDTO.getExperienceYears() : updateDeveloper.getExperienceYears();
compareLevelWithExperienceYears(developerLevel, experienceYears);
}
/**
* Validates the provided {@link CreateDeveloperRequestDTO} payload.
* It checks if the developer's experience years match the developer level.
* It also checks if a developer with the same memberId already exists.
*
* @param createDeveloperRequestDTO The payload containing the developer's information.
* @throws DMakerException If the payload validation fails.
*/
private void validationCreatePayload(CreateDeveloperRequestDTO createDeveloperRequestDTO) {
DeveloperLevel developerLevel = createDeveloperRequestDTO.getDeveloperLevel();
compareLevelWithExperienceYears(developerLevel, createDeveloperRequestDTO.getExperienceYears());
Optional<Developer> findDeveloper = developerRepository.findByMemberId(createDeveloperRequestDTO.getMemberId());
if (findDeveloper.isPresent()) {
throw new DMakerException(DMakerErrorCode.DUPLICATE_MEMBER_ID);
}
}
private static void compareLevelWithExperienceYears(DeveloperLevel developerLevel, Integer developerExperienceYears) {
if (developerLevel.equals(SENIOR) && developerExperienceYears < 10) {
throw new DMakerException(LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
}
if (developerLevel.equals(MIDDLE) &&
(developerExperienceYears < 4 || developerExperienceYears > 10)) {
throw new DMakerException(LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
}
if (developerLevel.equals(JUNIOR) && developerExperienceYears > 4) {
throw new DMakerException(LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
}
}
}
- 서비스 클래스 하나가 있다. 이 서비스 클래스는 두가지 의존성을 주입받는다.
- DeveloperRepository
- RetiredDeveloperRepository
DeveloperRepository
@Repository
public interface DeveloperRepository extends JpaRepository<Developer, Long> {
Optional<Developer> findByMemberId(String memberId);
List<Developer> findDevelopersByStatusCodeEquals(StatusCode statusCode);
}
RetiredDeveloperRepository
@Repository
public interface RetiredDeveloperRepository extends JpaRepository<RetiredDeveloper, Long> {
}
이런 관계를 가지고 있을 때, 테스트 코드를 작성해보자.
MockitoDMakerServiceTest
package cwchoiit.dmaker.service;
import cwchoiit.dmaker.dto.DeveloperDetailDTO;
import cwchoiit.dmaker.entity.Developer;
import cwchoiit.dmaker.repository.DeveloperRepository;
import cwchoiit.dmaker.repository.RetiredDeveloperRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static cwchoiit.dmaker.type.DeveloperLevel.SENIOR;
import static cwchoiit.dmaker.type.DeveloperType.BACK_END;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class MockitoDMakerServiceTest {
@Mock
private DeveloperRepository developerRepository;
@Mock
private RetiredDeveloperRepository retiredDeveloperRepository;
@InjectMocks
private DMakerService dMakerService;
@BeforeEach
void setUp() {
given(developerRepository.findByMemberId(anyString()))
.willReturn(Optional.of(Developer.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Samira")
.age(30)
.memberId("samira")
.build()));
}
@Test
void getAllEmployedDevelopers() {
DeveloperDetailDTO samira = dMakerService.getDeveloperByMemberId("samira");
assertThat(samira.getDeveloperLevel()).isEqualTo(SENIOR);
assertThat(samira.getDeveloperType()).isEqualTo(BACK_END);
assertThat(samira.getName()).isEqualTo("Samira");
assertThat(samira.getAge()).isEqualTo(30);
assertThat(samira.getMemberId()).isEqualTo("samira");
}
}
- 보통이라면 해당 서비스는 빈으로 등록된 서비스이기 때문에 주입을 받아야 하지만 Mockito를 사용해서 가짜로 주입받는다.
- Mockito를 사용하려면 먼저 다음 애노테이션이 필요하다.
@ExtendWith(MockitoExtension.class)
class MockitoDMakerServiceTest {...}
- @ExtendWith(MockitoExtension.class) 애노테이션을 클래스 레벨에 붙여준다.
그리고 아까 위에서 봤지만, DMakerService는 두가지 의존성을 주입 받는다. 그래서 이 DMakerService를 Mock으로 주입받기 위해선다음 작업이 필요하다.
@Mock
private DeveloperRepository developerRepository;
@Mock
private RetiredDeveloperRepository retiredDeveloperRepository;
@InjectMocks
private DMakerService dMakerService;
- 주입받을 의존성을 @Mock 애노테이션이 달린 상태로 선언한다.
- 실제로 이 테스트 클래스에 주입 받을 DMakerService를 Mock으로 주입하는데 이때 사용하는 애노테이션은 @InjectMocks이다.
- 그런데 만약, 저 두개의 의존성 중 이 테스트에서는 사용하지 않는 의존관계가 있다면 제거해도 무방하다. 다음 코드처럼.
@Mock
private DeveloperRepository developerRepository;
@InjectMocks
private DMakerService dMakerService;
이렇게 하면 가짜로 주입을 받을 수 있다. 이제 테스트를 위한 데이터가 필요하다. 어떻게 데이터를 가짜로 만들 수 있냐면 다음 코드를 보자.
@BeforeEach
void setUp() {
given(developerRepository.findByMemberId(anyString()))
.willReturn(Optional.of(Developer.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Samira")
.age(30)
.memberId("samira")
.build()));
}
- given() 메서드는 `org.mockito.BDDMockito.given`에서 제공하는 메서드이고, 사전 조건을 정의할 수 있다. 이때 developerRepository가 가지고 있는 findByMemberId() 메서드가 어떤 값을 받을 때 반환하는 가짜 데이터를 임의로 생성할 수 있다. 저기서는 anyString() 이라는 `org.mockito.ArgumentMatchers.anyString`에서 제공하는 메서드를 사용해서 어떤 문자열이 들어와도 동일한 반환을 할 수 있게 정의했다.
- findByMemberId는 반환값이 Optional이다. 그렇게 때문에 willReturn(Optional.of(...))을 사용한다.
- 이제 적절한 데이터를 만들어서 이 메서드가 호출되면 어떤 문자열이 들어와도 항상 코드에서 정의한 객체가 반환되도록 하였다.
이제 실제 테스트는 아래와 같이 작성하면 된다.
@Test
void getAllEmployedDevelopers() {
DeveloperDetailDTO samira = dMakerService.getDeveloperByMemberId("samira");
assertThat(samira.getDeveloperLevel()).isEqualTo(SENIOR);
assertThat(samira.getDeveloperType()).isEqualTo(BACK_END);
assertThat(samira.getName()).isEqualTo("Samira");
assertThat(samira.getAge()).isEqualTo(30);
assertThat(samira.getMemberId()).isEqualTo("samira");
}
- dMakerService.getDeveloperByMemberId() 메서드는 내부적으로 findByMemberId를 사용해서 데이터를 가져온다. 그렇게 되면 위 setUp() 메서드에서 정의한 가짜 데이터가 튀어나오게 될 것이다.
실행 결과
생성 관련 서비스 테스트
이번에는 좀 더 복잡한, 생성 관련 테스트를 진행해보자.
@Test
void createDeveloper_pass() {
CreateDeveloperRequestDTO xayah = CreateDeveloperRequestDTO.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Xayah")
.age(33)
.memberId("xayah")
.build();
// DMakerService.createDeveloper()를 호출할 때 검증 단계에서 실행되는 findByMemberId()
given(developerRepository.findByMemberId(anyString())).willReturn(Optional.empty());
// DMakerService.createDeveloper()를 호출할 때 DeveloperRepository.save()가 실행되는데, 그때 save()가 받는 Developer 객체를 Captor에 capture
ArgumentCaptor<Developer> captor = ArgumentCaptor.forClass(Developer.class);
// 서비스 호출
dMakerService.createDeveloper(xayah);
// DeveloperRepository.save()가 1번 호출되었는지 확인, save()가 받은 Developer 객체를 Captor에 저장
verify(developerRepository, times(1)).save(captor.capture());
// Captor에 저장된 값을 Retrieved
Developer newDeveloper = captor.getValue();
// 검증
assertThat(newDeveloper.getDeveloperLevel()).isEqualTo(SENIOR);
assertThat(newDeveloper.getDeveloperType()).isEqualTo(BACK_END);
assertThat(newDeveloper.getExperienceYears()).isEqualTo(15);
assertThat(newDeveloper.getName()).isEqualTo("Xayah");
assertThat(newDeveloper.getAge()).isEqualTo(33);
assertThat(newDeveloper.getMemberId()).isEqualTo("xayah");
}
- 먼저 생성할 CreateDeveloperRequestDTO로 저장할 객체를 만든다.
- DMakerService의 createDeveloper() 메서드는 다음과 같이 검증 로직이 존재한다.
private void validationCreatePayload(CreateDeveloperRequestDTO createDeveloperRequestDTO) {
DeveloperLevel developerLevel = createDeveloperRequestDTO.getDeveloperLevel();
compareLevelWithExperienceYears(developerLevel, createDeveloperRequestDTO.getExperienceYears());
Optional<Developer> findDeveloper = developerRepository.findByMemberId(createDeveloperRequestDTO.getMemberId());
if (findDeveloper.isPresent()) {
throw new DMakerException(DMakerErrorCode.DUPLICATE_MEMBER_ID);
}
}
- 이 검증 로직에는 DeveloperRepository의 findByMemberId()를 호출해서 생성하려는 Developer의 memberId가 이미 있는지 체크하는 부분이 있다. 그렇다는 것은 findByMemberId()에 대한 Mocking이 필요하다는 얘기이다.
- 그렇기 때문에 위 테스트 코드에서 다음 코드가 존재한다.
given(developerRepository.findByMemberId(anyString())).willReturn(Optional.empty());
- findByMemberId()가 호출되면, Optional.empty()를 반환하게 Mocking하여, 검증에 통과하도록 한다.
여기서 끝나면 안된다. 왜냐하면 생성 관련 테스트는 실제로 전달한 생성 데이터가 제대로 생성이 됐는지 확인을 해줘야 하기 때문이다. 그리고 제대로 생성된 데이터를 확인하기 위해 그 데이터를 보관할 객체를 Mockito는 제공한다.
// DMakerService.createDeveloper()를 호출할 때 DeveloperRepository.save()가 실행되는데, 그때 save()가 받는 Developer 객체를 Captor에 capture
ArgumentCaptor<Developer> captor = ArgumentCaptor.forClass(Developer.class);
- 이렇게 ArgumentCaptor<T>로 생성 객체를 저장할 수 있다. 우리가 확인하길 원하는 생성 객체의 타입은 Developer이므로, ArgumentCaptor<Developer> 타입으로 지정한다.
// 서비스 호출
dMakerService.createDeveloper(xayah);
- 서비스를 호출한다.
// DeveloperRepository.save()가 1번 호출되었는지 확인, save()가 받은 Developer 객체를 Captor에 저장
verify(developerRepository, times(1)).save(captor.capture());
- 호출한 서비스에서 DeveloperRepository의 save() 메서드가 1번 실행됐음을 확인한다.
- 그리고 해당 save()에 던져진 파라미터를 위에서 선언한 Captor에 저장한다.
// Captor에 저장된 값을 Retrieved
Developer newDeveloper = captor.getValue();
- Captor에 저장한 객체를 꺼내온다.
// 검증
assertThat(newDeveloper.getDeveloperLevel()).isEqualTo(SENIOR);
assertThat(newDeveloper.getDeveloperType()).isEqualTo(BACK_END);
assertThat(newDeveloper.getExperienceYears()).isEqualTo(15);
assertThat(newDeveloper.getName()).isEqualTo("Xayah");
assertThat(newDeveloper.getAge()).isEqualTo(33);
assertThat(newDeveloper.getMemberId()).isEqualTo("xayah");
- 해당 객체는 우리가 생성 테스트를 위해 만든 객체와 동일한 값들을 가지는지 확인한다.
생성 관련 테스트 (실패 케이스)
이번엔 생성 테스트 중 실패 케이스를 보자.
@Test
void createDeveloper_fail() {
// DMakerService.createDeveloper()를 호출할 때 검증 단계에서 실행되는 findByMemberId()
given(developerRepository.findByMemberId(anyString()))
.willReturn(Optional.of(Developer.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Xayah")
.age(33)
.memberId("xayah")
.build()));
CreateDeveloperRequestDTO xayah = CreateDeveloperRequestDTO.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Xayah")
.age(33)
.memberId("xayah")
.build();
// 서비스 호출 시 중복 memberId로 Developer 생성 시, DMakerException 발생
assertThatThrownBy(() -> dMakerService.createDeveloper(xayah)).isInstanceOf(DMakerException.class);
}
- 한 라인씩 알아보자.
// DMakerService.createDeveloper()를 호출할 때 검증 단계에서 실행되는 findByMemberId()
given(developerRepository.findByMemberId(anyString()))
.willReturn(Optional.of(Developer.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Xayah")
.age(33)
.memberId("xayah")
.build()));
- 이번엔 DeveloperRepository.findByMemberId()의 Mocking을 우리가 생성하고자 하는 데이터와 동일하게 해서 반환하도록 설정한다.
CreateDeveloperRequestDTO xayah = CreateDeveloperRequestDTO.builder()
.developerLevel(SENIOR)
.developerType(BACK_END)
.experienceYears(15)
.name("Xayah")
.age(33)
.memberId("xayah")
.build();
// 서비스 호출 시 중복 memberId로 Developer 생성 시, DMakerException 발생
assertThatThrownBy(() -> dMakerService.createDeveloper(xayah)).isInstanceOf(DMakerException.class);
- 그리고 생성 테스트를 위해 서비스의 createDeveloper()를 호출하면, DMakerException 예외를 터트릴 것을 예상한다.
- 왜냐하면, 검증 단계에서 같은 memberId가 있는 경우 다음과 같이 예외를 터트린다.
Optional<Developer> findDeveloper = developerRepository.findByMemberId(createDeveloperRequestDTO.getMemberId());
if (findDeveloper.isPresent()) {
throw new DMakerException(DMakerErrorCode.DUPLICATE_MEMBER_ID);
}
컨트롤러 테스트
이번엔 아래와 같은 컨트롤러에 대한 테스트도 진행해보자.
DMakerController
package cwchoiit.dmaker.controller;
import cwchoiit.dmaker.dto.*;
import cwchoiit.dmaker.service.DMakerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* This RESTful controller provides endpoints for retrieving developer names.
*/
@Slf4j
@RestController
@RequiredArgsConstructor
public class DMakerController {
private final DMakerService dMakerService;
/**
* Retrieves a list of developer names.
*
* @return A list of developer names.
*/
@GetMapping("/developers")
public List<DeveloperDTO> getDevelopers() {
log.info("GET /developers");
return dMakerService.getAllEmployedDevelopers();
}
@GetMapping("/developers/{memberId}")
public DeveloperDetailDTO getDeveloperByMemberId(@PathVariable String memberId) {
log.info("GET /developers/{}", memberId);
return dMakerService.getDeveloperByMemberId(memberId);
}
/**
* Creates a new developer.
*
* @param createDeveloperRequestDTO The request object containing the details of the developer to be created.
* @return A message indicating that the developer was created successfully.
*/
@PostMapping("/developers")
public CreateDeveloperResponseDTO createDeveloper(@Validated @RequestBody CreateDeveloperRequestDTO createDeveloperRequestDTO) {
log.info("POST /developers");
return dMakerService.createDeveloper(createDeveloperRequestDTO);
}
@PutMapping("/developers/{memberId}")
public DeveloperDetailDTO updateDeveloperByMemberId(@PathVariable String memberId,
@Validated @RequestBody UpdateDeveloperRequestDTO updateDeveloperRequestDTO) {
log.info("PUT /developers/{}", memberId);
return dMakerService.updateDeveloperByMemberId(memberId, updateDeveloperRequestDTO);
}
@DeleteMapping("/developers/{memberId}")
public DeveloperDetailDTO deleteDeveloperByMemberId(@PathVariable String memberId) {
log.info("DELETE /developers/{}", memberId);
return dMakerService.deleteDeveloperByMemberId(memberId);
}
}
테스트 코드를 작성하자.
DMakerControllerTest
package cwchoiit.dmaker.controller;
import cwchoiit.dmaker.dto.DeveloperDTO;
import cwchoiit.dmaker.service.DMakerService;
import cwchoiit.dmaker.type.DeveloperLevel;
import cwchoiit.dmaker.type.DeveloperType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(DMakerController.class)
class DMakerControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private DMakerService dMakerService;
private final MediaType contentType =
new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), StandardCharsets.UTF_8);
@BeforeEach
void setUp() {
DeveloperDTO samira = DeveloperDTO.builder()
.developerLevel(DeveloperLevel.SENIOR)
.developerType(DeveloperType.BACK_END)
.memberId("samira")
.build();
DeveloperDTO jin = DeveloperDTO.builder()
.developerLevel(DeveloperLevel.MIDDLE)
.developerType(DeveloperType.BACK_END)
.memberId("Jin")
.build();
given(dMakerService.getAllEmployedDevelopers()).willReturn(List.of(samira, jin));
}
@Test
void getAllDevelopers() throws Exception {
mockMvc.perform(get("/developers").contentType(contentType))
.andExpect(status().isOk())
.andDo(print())
.andExpect(jsonPath("$.[0].memberId").value("samira"))
.andExpect(jsonPath("$.[0].developerLevel").value("SENIOR"))
.andExpect(jsonPath("$.[1].memberId").value("Jin"))
.andExpect(jsonPath("$.[1].developerLevel").value("MIDDLE"));
}
}
- 컨트롤러 테스트만을 할건데, 모든 스프링 빈을 등록할 필요는 없을 것이다. 그래서 딱 원하는 빈만 스프링 컨테이너에 등록해서 테스트를 할 수 있게 해주는 기능이 있는데 바로 다음 애노테이션이다.
@WebMvcTest(DMakerController.class)
class DMakerControllerTest {...}
- 이렇게 @WebMvcTest(DMakerController.class) 라는 애노테이션을 등록하면, 이 테스트를 실행할 때 저 컨트롤러만 빈으로 등록되어 사용할 수 있다.
@Autowired
private MockMvc mockMvc;
@MockBean
private DMakerService dMakerService;
- HTTP REST 요청을 하기 위해 필요한 MockMvc
- DMakerController가 주입 받아야 하는 DMakerService를 Mock으로 빈 등록을 한다.
- 위에서 서비스 관련 테스트를 할때는 필요한 주입을 @Mock 애노테이션으로 사용했는데 여기서는 @MockBean으로 하는 이유는 이 테스트를 실행해보면 알겠지만 스프링이 띄워진다. @WebMvcTest는 스프링을 띄우긴 띄우는데 선언한 빈들만 등록하게 해서 띄우는 간단한 스프링이라고 생각하면 좋다. 그래서 스프링 Bean으로 등록될 Mock을 선언하는 @MockBean 애노테이션을 선언한다.
@BeforeEach
void setUp() {
DeveloperDTO samira = DeveloperDTO.builder()
.developerLevel(DeveloperLevel.SENIOR)
.developerType(DeveloperType.BACK_END)
.memberId("samira")
.build();
DeveloperDTO jin = DeveloperDTO.builder()
.developerLevel(DeveloperLevel.MIDDLE)
.developerType(DeveloperType.BACK_END)
.memberId("Jin")
.build();
given(dMakerService.getAllEmployedDevelopers()).willReturn(List.of(samira, jin));
}
- 테스트를 실행하기 앞서, 테스트 데이터가 필요하므로 테스트 데이터를 생성한다. 그래서 DMakerService의 getAllEmployedDevelopers()가 호출되면 반환되는 리스트를 가짜로 만든다.
@Test
void getAllDevelopers() throws Exception {
mockMvc.perform(get("/developers").contentType(contentType))
.andExpect(status().isOk())
.andDo(print())
.andExpect(jsonPath("$.[0].memberId").value("samira"))
.andExpect(jsonPath("$.[0].developerLevel").value("SENIOR"))
.andExpect(jsonPath("$.[1].memberId").value("Jin"))
.andExpect(jsonPath("$.[1].developerLevel").value("MIDDLE"));
}
- 실제 테스트 코드는 다음과 같다. 주입 받은 MockMvc를 사용해서 HTTP 요청을 날린다. 이때 Content-Type은 JSON으로 지정하기 위해 필드로 선언한 `contentType`을 사용한다. 그리고 그 응답 결과를 우리가 만든 가짜 데이터가 나올 것으로 예상한 테스트 코드를 작성한다.
- andDo(print())는 이 요청에 대한 요청-응답 정보를 출력하는 메서드이다.
실행 결과
정리를 하자면
이렇게 간단하게 데이터베이스가 필요한 서비스지만, 데이터베이스를 사용하고 싶지 않을 때 Mockito를 사용해서 가짜로 데이터를 만들어 내는 방법을 배워봤다. 간단한 단위 테스트를 작성할 때 Mockito를 사용하면 코드 자체적인 문제를 잘 찾아낼 수 있을 것 같다.
'Spring Advanced' 카테고리의 다른 글
TestContainer를 이용한 Spring Boot 테스트 (2) | 2024.10.03 |
---|---|
Redis를 사용해서 캐싱하기 (0) | 2024.10.02 |
AOP (Part. 3) - 포인트컷 (0) | 2024.01.02 |
AOP (Part.2) (0) | 2024.01.02 |
AOP(Aspect Oriented Programming) (0) | 2023.12.29 |