Spring Boot 레이어별 테스트 작성 가이드
- Domain
- JUnit, AssertJ 등 테스트 편의 도구를 이용해서 테스트한다.
- Repository
- 인메모리 DB를 사용하고, JPA 관련 설정만 불러오는 @DataJpaTest를 이용해서 Repository의 동작(저장 및 조회)에 대해 테스트한다.
- 저장을 위한 JPA 연관 관계가 적절히 구성되었는지, Repository 메소드가 제대로 구현되었는지 확인하는 것을 목적으로 한다.
- JPA 관련 어노테이션 없이 코드를 작성해서 저장에 실패하는 테스트 코드를 먼저 작성하고, JPA 규칙에 맞는 어노테이션을 추가해서 테스트 코드를 통과시킨다.
- 기본적으로 인메모리 DB를 사용하나 테스트에 사용할 DB를 지정할 수 있다.
- Service
- 트랜잭션을 관리하는 것이 주요 책임(CRUD에 대한 책임은 Repository에 역임)
- Repository의 동작은 Repository Test에서 확인했으므로 Repository의 메소드가 제대로 호출되는지만 확인하면 되므로 실제 Repository가 아닌 Mock Repository를 사용한다.
- Mockito를 사용하므로 실제 저장/조회가 발생하지 않는다.
- 생성, 삭제, 조회 시에는 생성/삭제/조회를 호출하는 메서드 호출 여부를 verify 하고, 수정 시에는 명시적으로 save()나 saveAndFlush()가 호출되지 않을 수도 있으므로 verify 로는 테스트가 불가능하므로 Mock 에 사용되는 엔티티나 DTO의 상태 변경을 assert 한다.
- Controller
- 넓게 보면 사용자가 보낸 요청을 서비스에 전달하기까지 모든 과정을 포괄한다고 볼 수 있지만, 필터, 인터셉터, 요청 라우팅, 보안(인증, 인가), 데이터 validation, 데이터 바인딩 등은 스프링 프레임워크에서 담당해주므로 컨트롤러가 실제로 담당하는 부분은 이를 모두 통과한 후의 요청 데이터를 서비스에 전달해주고, 서비스가 반환하는 결과를 클라이언트에게 반환하는 부분
- 컨트롤러의 로직이 많지 않은 경우 서비스를 Mock 해서 컨트롤러 레이어만을 단위 테스트하는 것은 효용이 크지 않을 수 있으므로, 컨트롤러 테스트는 보통 Mock 대신 실제 서비스 및 도메인 계층을 대상으로 통합 테스트로 작성하는 편이 낫다.
참고자료
- https://github.com/HomoEfficio/dev-tips/blob/master/Spring-Boot-레이어별-테스트.md
- https://meetup.toast.com/posts/124
레이어별 기본 테스트 코드
1. 도메인 테스트
@DataJpaTest
public class DomainSampleTest{
@Autowired
private SomethingRepository somethingRepository;
@Test
@DisplayName("")
public void test(){
// given
Something something = somethingRepository.save(Something.builder()
.name("test")
.build());
// when
Optional<Something> result = somethingRepository.findById(1L);
// then
assertAll(
() -> assertEquals(result.getId(), 1L),
() -> assertEquals(result.getName(), "test")
);
}
}
@DataJpaTest 어노테이션을 이용해 인메모리 DB를 사용하고, JPA 관련 설정을 불러와서 테스트한다.
2. Service 테스트
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.BDDMockito.*;
@ExtendWith(MockitoExtension.class)
public class MyServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
@DisplayName("")
public void findById(){
// given
Long testId = 1L;
given(userRepository.findById(anyLong()).willReturn(Optional.empty());
// when
assertThrows(EntityNotFoundException.class, () -> userService.findById(testId));
// then
verify(userRepository).findById(anyLong());
}
}
- 레포지토리와 서비스를 각각 mocking하여 가짜 객체로 테스트를 진행한다.
- 성공과 실패 테스트 모두 작성하고, 각각의 예외 상황이 잘 처리 되는지 확인한다.
- verify를 통해 레포지토리의 메소드가 수행되었는지 검증할 수 있다.
3. 컨트롤러 테스트
@Transactional
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest {
@LocalServerPort
private int port;
@Autowired
private MockMvc mvc;
@MockBean
private UserService userService;
private static UserDetails userDetails;
@BeforeAll
public static void init() {
userDetails = UserDetailsVO.builder()
.id(1L)
.name("test")
.role(UserRole.ADMIN)
.build();
}
@Test
@DisplayName("어드민은_어떤_서비스를_이용한다_성공")
void findAllUser() throws Exception {
// given
given(userService.findAll(anyLong())).willReturn(APIResult.OK());
// when
String url = "http://localhost:" + port + "/api/user";
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.delete(url)
.contentType(MediaType.APPLICATION_JSON)
.with(user(userDetails));
// then
mvc.perform(requestBuilder)
.andDo(print())
.andExpect(status().isOk());
}
}
- Service를 MockBean으로 등록(@Mock과 @MockBean의 차이점은 하단에)
- Service의 메소드를 실행한 상황을 given으로 가정하고 어떤 결과를 예측(assert)한다.
- MockBean
- 가짜 Bean을 스프링에 등록
- 스프링 컨테이너가 기존에 갖고있는 Bean객체는 MockBean객체로 치환되어 DI에 사용됩니다.
- Mock
- 가짜 객체를 만들고, 스프링빈에 등록이 안되는 객체
- 스프링 컨테이너가 DI를 하는 방식이 아니라 객체 생성 시 생성자에 Mock객체를 직접 주입
- 생성자 주입을 사용해야 편하게 사용 가능
- 스프링을 띄우지 않으므로 MockBean을 사용할때보다 빠르다
728x90
'# Back-End > Spring' 카테고리의 다른 글
JWT란? (0) | 2022.01.23 |
---|---|
인증 방식 비교(서버 기반 인증, 토큰 기반 인증) (0) | 2022.01.23 |
Filter, Interceptor, AOP (필터, 인터셉터, AOP) (0) | 2022.01.23 |
[Test] Spring Boot 테스트 클래스 정의 어노테이션 (0) | 2022.01.23 |
[Test] JUnit5를 이용한 테스트 코드 작성 (0) | 2022.01.23 |
H2 console 세팅 & 접속 (0) | 2021.11.19 |
[JPA] 연관 관계를 가진 엔티티의 생성 (0) | 2021.10.14 |