스프링에서는 어떻게 단위 테스트를 했는지 내가 하고 있는 프로젝트를 예시로 들면서 작성해 보겠다.
하지만 본인도 공부하면서 해보고 있는 거라 틀릴 수도 있다는 점... 적용하고 있는 거라 틀린 부분 있으면 지적해 주시면 감사하겠습니다.
인프런에서 김영한 강사님의 강의를 들을 때 배운 given/when/then 방식을 사용해서 만들었다.
여기서 말한 given/when/then을 간단하게 설명한다면 이와 같다.
given: 테스트의 사전 조건 설정
when: 테스트 대상 동작 수행
then: 예상 결과 검증
나는 테스트해야 하는 클래스가 크게 controller, repository, service 3개, 그리고 각각의 클래스에 각각에 맞게 단위 테스트를 하고 모든 과정을 테스트하는 통합테스트 1개 이렇게 총 4개를 만들었다.
컨트롤러(Controller) 테스트
- 컨트롤러는 HTTP 요청을 처리하고 클라이언트와 상호 작용한다. 따라서 컨트롤러를 테스트할 때는 HTTP 요청 및 응답을 모의테스트 한다.
- 테스트는 'MockMvc'를 사용했다. 이를 통해 컨트롤러의 엔드포인트를 호출하고 결과를 나타내는 방식으로 했다.
private MockMvc mockMvc;
@InjectMocks
private RestaurantController restaurantController;
@Mock
private RestaurantService restaurantService;
private ObjectMapper objectMapper;
@BeforeEach
public void setUp() {
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(restaurantController).build();
this.objectMapper = new ObjectMapper();
}
@Test
@DisplayName("식당 등록 - 성공 요청")
void registerRestaurant_ValidRequest() throws Exception {
// Given
RegisterTypeRequestDto typeRequestDto = new RegisterTypeRequestDto("한식");
MvcResult typeResult = mockMvc.perform(
post("/restaurants/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(typeRequestDto)))
.andExpect(status().isCreated())
.andReturn();
Long resTypeId = Long.parseLong(typeResult.getResponse().getContentAsString());
RegisterRestaurantRequestDto requestDto = new RegisterRestaurantRequestDto("새로운식당", "해당URL", "주소",
"메뉴이미지", resTypeId, "대표메뉴", "상세");
// When, Then
mockMvc.perform(
post("/restaurants")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestDto)))
.andExpect(status().isCreated());
verify(restaurantService).registerRestaurant(requestDto);
}
Service 테스트
- 서비스는 비즈니스 로직을 처리하고, 주로 컨트롤러와 리포지토리 사이에서 중간 계층 역할을 한다. 서비스 테스트는 비즈니스 로직의 정확성을 검증한다.
- 서비스 테스트에서는 주로 서비스 메서드를 호출하고, 입력 값에 따른 결과를 검증한다.
- 따라서 서비스 테스트에서는 주로 모의(Mock) 객체 또는 스파이(Spy)를 사용하여 협력 객체를 대체하거나, 실제 데이터를 사용하지 않는 방법으로 테스트한다.
@ExtendWith(MockitoExtension.class)
class RestaurantServiceTest {
@InjectMocks
private RestaurantService restaurantService;
@Mock
private RestaurantTypeJpaRepository restaurantTypeRepository;
@Mock
private RestaurantJpaRepository restaurantRepository;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
@DisplayName("음식점 타입 등록 테스트")
void 음식점_타입_등록() {
// given
RegisterTypeRequestDto typeRequestDto = RegisterTypeRequestDto.builder()
.resType("한식")
.build();
when(restaurantTypeRepository.save(any(RestaurantType.class))).thenReturn(new RestaurantType());
// when
Long resTypeId = restaurantService.registerType(typeRequestDto);
// then
assertThat(resTypeId).isNotNull();
}
Repository 테스트
- 레포지토리는 데이터베이스와 상호 작용하며 데이터베이스 쿼리와 관련된 로직을 처리한다.
- 주로 데이터베이스 내용을 초기화하고, 레포지토리 메서드를 호출한 후 결과를 검증한다.
- @DataJpaTest 어노테이션을 사용하면 기본적으로 인메모리 데이터베이스인 H2를 기반으로 테스트용 데이터베이스를 구축하고, 테스트가 끝나면 트랜잭션 롤백을 해준다.
@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource("classpath:application-test.properties")
class RestaurantJpaRepositoryTest {
@Autowired
private RestaurantTypeJpaRepository restaurantTypeRepository;
@Autowired
private RestaurantJpaRepository restaurantRepository;
@Test
@DisplayName("음식점_타입_등록_테스트")
@Transactional
void 음식점_타입_등록() {
// given
RestaurantType restaurantType = RestaurantType.builder()
.resType("한식")
.build();
//when
RestaurantType saveType = restaurantTypeRepository.save(restaurantType);
//then
assertThat(restaurantType.getResType())
.isEqualTo(saveType.getResType());
}
통합 테스트
@SpringBootTest(properties = "spring.config.location=classpath:application-test.properties")
@AutoConfigureMockMvc
class RestaurantIntegrationTest {
@Autowired
private MockMvc mockMvc;
ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private RestaurantService restaurantService;
@Autowired
private RestaurantJpaRepository restaurantRepository;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(new RestaurantController(restaurantService))
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.build();
}
@Test
@DisplayName("음식점 등록 통합 테스트")
void 음식점_등록_통합_테스트() throws Exception {
// given
RegisterTypeRequestDto requestDto = RegisterTypeRequestDto.builder()
.resType("한식")
.build();
Long resTypeId = restaurantService.registerType(requestDto);
RegisterRestaurantRequestDto restaurantRequestDto = RegisterRestaurantRequestDto.builder()
.resName("새로운 음식점")
.resImage("이미지 URL")
.resAddress("주소")
.resMenuImage("메뉴 이미지")
.resType(resTypeId)
.resBestMenu("대표 메뉴")
.resDetail("디테일")
.build();
// when
mockMvc.perform(post("/restaurants")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(restaurantRequestDto)))
.andExpect(status().isCreated());
// then
Restaurant savedRestaurant = restaurantRepository.findByResName("새로운 음식점");
assertThat(savedRestaurant).isNotNull();
assertThat(savedRestaurant.getResName()).isEqualTo("새로운 음식점");
assertThat(savedRestaurant.getResImage()).isEqualTo("이미지 URL");
assertThat(savedRestaurant.getResAddress()).isEqualTo("주소");
assertThat(savedRestaurant.getResMenuImage()).isEqualTo("메뉴 이미지");
assertThat(savedRestaurant.getResType().getResTypeId()).isEqualTo(resTypeId);
assertThat(savedRestaurant.getResBestMenu()).isEqualTo("대표 메뉴");
assertThat(savedRestaurant.getResDetail()).isEqualTo("디테일");
}
}
따라서..
이렇게 정리해 보니 나도 제대로 규칙도 적용 안 하고 통일되게 만들지 않았다... 다시 고치면서 한번 더 깔끔하게 적용해야겠다는 생각이 들었다..
테스트는 소프트웨어 개발 과정에서 핵심적이며 어떻게 보면 가장 중요한 역할을 한다.
단위 테스트와 통합 테스트를 통해 코드의 신뢰성을 높이고 버그를 미리 발견할 수 있다.
이러한 방법을 적절하게 활용하면 개발 과정이 훨씬 원활해진다.(하지만 나는 아직 이게 더 오래 걸리는 느낌ㅜ,,,)
조금 더 깊게 보고 싶은 분들은 이 글들을 참고하면 좋을 것 같다.
'Spring' 카테고리의 다른 글
[Spring] java.lang.IllegalArgumentException: Name for argument of type [java.lang.Long] not specified, and parameter name information not available via reflection. (0) | 2024.02.06 |
---|---|
단위 테스트, 통합 테스트 (0) | 2023.09.14 |
DDD(Domain Driven Desgin) (0) | 2023.07.25 |
프로젝트 패키지 구조는 어떻게 나누는게 좋을까 (0) | 2023.07.14 |
[Spring] 왜 Service 와 ServiceImpl로 나누는걸까 (0) | 2023.06.28 |