# Back-End/Spring

[Test] Spring Layer별 테스트 작성

Spring Boot 레이어별 테스트 작성 가이드
  1. Domain
    • JUnit, AssertJ 등 테스트 편의 도구를 이용해서 테스트한다.
  2. Repository
    • 인메모리 DB를 사용하고, JPA 관련 설정만 불러오는 @DataJpaTest를 이용해서 Repository의 동작(저장 및 조회)에 대해 테스트한다.
    • 저장을 위한 JPA 연관 관계가 적절히 구성되었는지, Repository 메소드가 제대로 구현되었는지 확인하는 것을 목적으로 한다.
    • JPA 관련 어노테이션 없이 코드를 작성해서 저장에 실패하는 테스트 코드를 먼저 작성하고, JPA 규칙에 맞는 어노테이션을 추가해서 테스트 코드를 통과시킨다.
    • 기본적으로 인메모리 DB를 사용하나 테스트에 사용할 DB를 지정할 수 있다.
  3. Service
    • 트랜잭션을 관리하는 것이 주요 책임(CRUD에 대한 책임은 Repository에 역임)
    • Repository의 동작은 Repository Test에서 확인했으므로 Repository의 메소드가 제대로 호출되는지만 확인하면 되므로 실제 Repository가 아닌 Mock Repository를 사용한다.
    • Mockito를 사용하므로 실제 저장/조회가 발생하지 않는다.
    • 생성, 삭제, 조회 시에는 생성/삭제/조회를 호출하는 메서드 호출 여부를 verify 하고, 수정 시에는 명시적으로 save()나 saveAndFlush()가 호출되지 않을 수도 있으므로 verify 로는 테스트가 불가능하므로 Mock 에 사용되는 엔티티나 DTO의 상태 변경을 assert 한다.
  4. Controller
    1. 넓게 보면 사용자가 보낸 요청을 서비스에 전달하기까지 모든 과정을 포괄한다고 볼 수 있지만, 필터, 인터셉터, 요청 라우팅, 보안(인증, 인가), 데이터 validation, 데이터 바인딩 등은 스프링 프레임워크에서 담당해주므로 컨트롤러가 실제로 담당하는 부분은 이를 모두 통과한 후의 요청 데이터를 서비스에 전달해주고, 서비스가 반환하는 결과를 클라이언트에게 반환하는 부분
    2. 컨트롤러의 로직이 많지 않은 경우 서비스를 Mock 해서 컨트롤러 레이어만을 단위 테스트하는 것은 효용이 크지 않을 수 있으므로, 컨트롤러 테스트는 보통 Mock 대신 실제 서비스 및 도메인 계층을 대상으로 통합 테스트로 작성하는 편이 낫다.

참고자료

  1. https://github.com/HomoEfficio/dev-tips/blob/master/Spring-Boot-레이어별-테스트.md
  2. 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