JUnit과 Mocktio 기반의 Spring 단위 테스트

나도잘몰라·2024년 4월 10일
0

spring

목록 보기
2/5

1. 테스트

1.1. 단위 테스트와 통합 테스트

단위 테스트(Unit Test)

  • 하나의 모듈(기능 또는 메소드) 기준으로 독립적으로 진행되는 가장 작은 단위의 기능 검증 테스트
  • 다른 객체와 호환되어야 할 경우 독립적으로 테스트하기 위해 가짜 객체(Mock Object)를 주입하여 어떤 결과를 반환하라고 정해진 답변을 준비시킴(stub)

통합 테스트(Integration Test)

  • 모듈을 통합하는 과정에서 모듈 간의 호환성을 확인하기 위해 수행되는 테스트
  • 애플리케이션 설정과 Bean들을 모두 로드해 운영환경과 가장 유사한 테스트가 가능
  • 시간이 오래 걸리고 무거움
  • @SpringBootTest

1.2. F.I.R.S.T 테스트 원칙

  • Fast : 빠르게 실행
    • @SpringBootTest는 해당 어플리케이션의 모든 빈을 IoC Container에 등록하고 테스트를 진행하기 때문에 테스트가 느려질 수 밖에 없음
  • Independent : 각각의 테스트는 독립적이며 객체의 상태, 메소드, 이전 테스트 상태, 다른 메소드의 결과 등에 의존해서는 안 됨
    • 단위테스트는 어떠한 순서로 실행하더라도 성공
  • Repeatable : 반복 가능
    • DB에 의존하여 여러번 실행하면 실패하는 경우 X
  • Self-Validating : 테스트는 직접 수동으로 확인하는 것이 아닌 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증
    • 성공 테스트만 작성하는 것이 아님
  • Timely : 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.

1.3 테스트 라이브러리

  • JUnit5 : 자바 단위 테스트
    • JUnit에서 제공하는 assertEquals()와 같은 메소드는 AssertJ가 주는 메소드에 비해 가독성이 떨어짐
  • AssertJ : 자바 테스트를 돕기 위해 다양한 문법을 지원
  • Mockito : 개발자가 동작을 직접 제어할 수 있는 가짜 객체를 지원

1.4. Mockito

1.4.1. Mock 어노테이션

  • @ExtendWith(MockitoExtension.class) : SpringBoot 2.2.0부터 공식적으로 JUnit5를 지원함에 따라 해당 어노테이션을 통해 Mockito와 JUnit 연동. 가짜 환경 세팅
  • @Mock : 스텁 처리되는 가짜 객체를 반환
  • @Spy : 원본 메소드 그대로 사용하기 위해 진짜 객체를 반환
  • @InjectMocks : @Mock 또는 @Spy로 생성된 객체를 자동으로 주입

1.4.2. Stub 메소드

  • doReturn() : 가짜 객체가 특정한 값을 반환해야 하는 경우
  • doNothing() : 가짜 객체가 아무 것도 반환하지 않는 경우(void)
  • doThrow() : 가짜 객체가 예외를 발생시키는 경우
  • then~()
    • 메소드를 실제 호출 > 메소드 작업이 오래 걸릴 경우 끝날때까지 기다림. 대상 메소드에 문제점이 있을 경우 발견 가능.
  • do~()
    • 메소드를 실제 호출 X > 컴파일 시 리턴 타입 체크가 안 됨. 실제 메소드를 호출하지 않기 때문에 대상 메소드에 문제점이 있어도 알수가 없음
    • doNothing()을 통해 void 메서드 stubbing 가능
    • spy는 mock처럼 위임된 메서드를 호출하는 것이 아니라 실제 인스턴스의 메서드를 호출하기 때문에 doReturn 권장

1.5. 테스트 패턴

  • given(준비): 어떠한 데이터가 준비되었을 때
  • when(실행): 어떠한 함수를 실행하면
  • then(검증): 어떠한 결과가 나와야 한다.

2. Controller Test

2.1. MockMvc와 WebMvcTest

  • MockMvc : 컨트롤러를 테스트에 HTTP 호출을 제공

    @ExtendWith(MockitoExtension.class)
    class UserControllerTest {
    
        @InjectMocks
        private UserController userController;
    
        @Mock
        private UserService userService;
    
        private MockMvc mockMvc;
    
        @BeforeEach
        public void init() {
            mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
        }
    
    }

    꿀팁!!!!!!!!!

    doThrow(new CustomException()).when().save());

    @ControllerAdvice에서 처리되는 커스텀 Exception을 테스트 할 때는 MockMvc에 @ControllerAdvice 정보도 설정해줍니다.

    @BeforeEach
        public void init() {
            mockMvc = MockMvcBuilders.standaloneSetup(registrationController)
                    .setControllerAdvice(new YourControllerAdvice())
                    .build();
        }

    그렇지 않으면 아래와 같은 테스트 실패를 겪을 것입니다!!!!!

    jakarta.servlet.ServletException: Request processing failed: com.*.*.exception.*Exception

    수고~ ><

  • @WebMvcTest : MockMvc 객체가 자동으로 생성되며 ControllerAdvice나 Filter, Interceptor 등 웹 계층 테스트에 필요한 요소들을 모두 빈으로 등록해 스프링 컨텍스트 환경을 구성. 스프링부트가 제공하는 테스트 환경이므로 @Mock@Spy 대신 각각 @MockBean@SpyBean을 사용.

    • 스프링은 내부적으로 스프링 컨텍스트를 캐싱해두고 동일한 테스트 환경이라면 재사용한하는데 특정 컨트롤러만을 빈으로 만들고 @MockBean@SpyBean으로 빈을 모킹하는 @WebMvcTest는 캐싱의 효과를 거의 얻지 못하고 새로운 컨텍스트의 생성 필요. 빠른 테스트를 원한다면 직접 MockMvc를 생성하는 방법을 사용하는 것이 좋을 수 있음.

      @WebMVcTest(UserController.class)
      class UserControllerTest {
      
          @MockBean
          private UserService userService;
      
          @Autowired
          private MockMvc mockMvc;
      
      }

2.2. 예시

@ExtendWith(MockitoExtension.class)
  class UserControllerTest {

	@InjectMocks
	private UserController userController;

	@Mock
	private UserService userService;

	private MockMvc mockMvc;

	@BeforeEach
	public void init() {
		mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
	}

    @DisplayName("회원 가입 성공")
    @Test
    void signUpSuccess() throws Exception {
        // given
        SignUpRequest request = signUpRequest();
        UserResponse response = userResponse();

        doReturn(response).when(userService)
            .signUp(any(SignUpRequest.class));

        // when
        ResultActions resultActions = mockMvc.perform(
            MockMvcRequestBuilders.post("/users/signUp")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(new Gson().toJson(request))
        );

        // then
        MvcResult mvcResult = resultActions.andExpect(status().isOk())
            .andExpect(jsonPath("email", response.getEmail()).exists())
            .andExpect(jsonPath("pw", response.getPw()).exists())
            .andExpect(jsonPath("role", response.getRole()).exists())
    }
}
  • @Test : 테스트 인식
  • @DisplayName : default로 저장되는 테스트 이름을 커스텀
  • any()
    • (HTTP 요청을 보내면 Spring은 내부에서 MessageConverter를 사용해 Json String을 객체로 변환하는데 이것은 Spring 내부에서 진행되므로) API로 전달되는 파라미터인 SignUpRequest를 조작할 수 없음 > SignUpRequest 클래스 타입이라면 어떠한 객체도 처리할 수 있도록 any()가 사용
    • any()를 사용할 때에는 특정 클래스의 타입을 지정해주는 것이 좋음
  • MockMvcRequestBuilders
    • 요청 메소드 종류, 내용, 파라미터 등 설정
    • 보내는 데이터는 객체가 아닌 문자열이여야 하므로 별도의 변환이 필요하므로 Gson을 사용해 변환

3. Service Test

@DisplayName("회원 가입")
@Test
void signUp() {
    // given
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    SignUpRequest request = signUpRequest();
    String encryptedPw = encoder.encode(request.getPw());

    doReturn(new User(request.getEmail(), encryptedPw, UserRole.ROLE_USER)).when(userRepository)
        .save(any(User.class));
        
    // when
    UserResponse user = userService.signUp(request);

    // then
    assertThat(user.getEmail()).isEqualTo(request.getEmail());
    assertThat(encoder.matches(signUpDTO.getPw(), user.getPw())).isTrue();

    // verify
    verify(userRepository, times(1)).save(any(User.class));
    verify(passwordEncoder, times(1)).encode(any(String.class));
}
  • verify() : Mockito 라이브러리를 통해 만들어진 가짜 객체의 특정 메소드가 호출된 횟수를 검증 가능

4. Repository Test

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @DisplayName("사용자 추가")
    @Test
    void addUser() {
        // given
        User user = user();
        
        // when
        User savedUser = userRepository.save(user);

        // then
        assertThat(savedUser.getEmail()).isEqualTo(user.getEmail());
        assertThat(savedUser.getPw()).isEqualTo(user.getPw());
        assertThat(savedUser.getRole()).isEqualTo(user.getRole());
    }

    private User user() {
        return User.builder()
                .email("email")
                .pw("pw")
                .role(UserRole.ROLE_USER).build();
    }
}
  • @DataJpaTest
    • JPA 리포지토리를 쉽게 테스트 가능
    • @Entity가 있는 클래스들을 스캔
    • 기본적으로 인메모리 데이터베이스인 H2를 기반으로 테스트용 데이터베이스를 구축하며, 테스트가 끝나면 트랜잭션 롤백
    • 레포지토리 계층은 실제 DB와 통신없이 단순 모킹하는 것은 의미가 없으므로 직접 데이터베이스와 통신하는 @DataJpaTest를 사용

참고

0개의 댓글