단위테스트에서 @SpringBootTest 사용하지 않기

sieun·2021년 7월 22일
2
post-thumbnail

개요

e-commerce 대용량 서버 프로젝트를 진행하면서 테스트 코드에 대해 공부하다 @SpringBootTest를 사용하는 테스트와 Mockito라이브러리로 구성된 테스트의 차이가 궁금하였습니다. 이를 알아보던 중, 단위테스트에서는 @SpringBootTest를 지양하는 것이 권장됨을 깨달았습니다.


@SpringBootTest

@SpringBootTest는 SpringApplication(@SpringBootApplication)을 통해 테스트에서 사용할 ApplicationContext를 쉽게 생성할 수 있습니다. 즉, @SpringBootApplication이 붙은 애너테이션을 찾아 하위의 모든 빈들을 scan합니다. 결국 애플리케이션에 정의된 모든 빈을 생성하기 때문에 실행 시 느려질 수 있어 통합테스트가 아닌 단위테스트에서는 적합하지 않습니다.


@MockBean

@MockBean은 Spring ApplicationContext에 Mock객체를 빈으로 등록하는데 사용되는 애너테이션입니다. 즉, Spring영역에 있는 애너테이션이기 때문에 Spring Context를 실행해야합니다. 그렇기에 @Autowired처럼 스프링이 제공하는 의존성 해결방법으로만 해당 Mock객체를 찾을 수 있어, 주로 @SpringBootTest이나 @WebMvcTest와 같이 사용될 수 있습니다.

✅ 공식문서를 참고해보니 구동에 필요한 거의 모든 빈들을 로딩하는 @SpringBootTest와는 달리 @WebMvcTest요청을 처리하기 위한 빈들로 제한하여 로딩합니다. 따라서 @WebMvcTest를 사용할 수 있다면 보다 더 가볍게 테스트할 수 있습니다.

또한 @MockBean과 같은 애너테이션을 사용하지 않고 테스트 코드를 작성하는것이 힘들다면 너무 많은 의존성을 가진 냄새나는 코드가 아닌지 의심해보아야 할 것 입니다.


Code

해당 프로젝트는 다음 github에서 확인 가능합니다.
https://github.com/f-lab-edu/online-marketplace

처음에 저희 프로젝트의 UserServiceTest클래스는 다음과 같은 형식으로 작성되었었습니다.

UserServiceTest.class

@ExtendWith(MockitoExtension.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserRepository userRepository;

    @DisplayName("중복되지 않은 이메일이면 회원가입에 성공한다.")
    @Test
    void signUp() {
        // given
        final SignUpRequestDto dto = SignUpRequestDto.builder()
                .name(User1.NAME)
                .email(User1.EMAIL)
                .password(User1.PASSWORD)
                .phone(User1.PHONE)
                .build();
        final Optional<User> notFoundUser = Optional.ofNullable(null);

        given(userRepository.findByEmail(any())).willReturn(notFoundUser);

        // when
        userService.join(dto);

        // then
        then(userRepository).should(times(1)).insertUser(any());
    }
...

위에서 언급한 것과 같이 @SpringBootTest를 사용하게 되면 어플리케이션에 정의된 모든 빈을 생성하므로 단위테스트로서는 무겁습니다.


Mockito로 전환된 Code

해당 단위테스트의 관심사는 UserService내의 join()메소드입니다.
이 메소드의 하위 메소드를 호출하여 원하는 결과값을 얻는지에 초점을 맞추면 됩니다.

join() 메소드

public void join(SignUpRequestDto dto){
        if (checkIsUserExist(dto.getEmail())) {
            throw new IllegalArgumentException("이미 등록된 메일입니다.");
        }

        String salt = SaltGenerator.generateSalt();
        CryptoData cryptoData = CryptoData.WithSaltBuilder()
                .plainText(dto.getPassword())
                .salt(salt)
                .build();
        String encryptedPassword = encryptor.encrypt(cryptoData);
        User user = dto.toEntity(salt, encryptedPassword);

        userRepository.insertUser(user);
    }

해당 메소드를 보면, 결국에는 insertUser()이 호출하는 것이 목적인 것을 알 수 있습니다. 그 과정에서 필요한 Mocking 대상은 UserRepositoryEncryptor에 대한 것입니다.

Mockito를 활용하면 해당 클래스에 필요한 mock객체를 생성하여 의존성을 해결할 수 있습니다.
Mockito.mock()을 이용할 수 있지만, 저는 좀 더 간결한 @Mock을 사용하여 mock객체를 생성해보려고 합니다. 그 후, @InjectMocks로 지정한 클래스안에 정의된 mock객체를 찾아 클래스의 객체가 만들어질 때 사용하여 주입하도록 작성하였습니다.

✅ JUnit5에서는 @Mock@ExtendWith(MockitoExtension.class)와 함께 사용되어야 테스트가 시작하기 전에 애너테이션을 감지할 수 있습니다.

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @InjectMocks
    private UserService userService;

    @Mock
    private UserRepository userRepository;

    @Mock
    private Encryptor encryptor;

    @DisplayName("중복되지 않은 이메일이면 회원가입에 성공한다.")
    @Test
    void signUp() {
        // given
        final SignUpRequestDto dto = SignUpRequestDto.builder()
                .name(User1.NAME)
                .email(User1.EMAIL)
                .password(User1.PASSWORD)
                .phone(User1.PHONE)
                .build();
        final Optional<User> notFoundUser = Optional.ofNullable(null);

        given(userRepository.findByEmail(any())).willReturn(notFoundUser);

        // when
        userService.join(dto);

        // then
        then(userRepository).should(times(1)).insertUser(any());

    }
}

후기

통합테스트와 단위테스트가 지닌 목적을 구분하여 쓰임에 맞는 자료를 정하는 것이 테스트 성능을 높이는데 도움이 되는 것을 확인할 수 있었습니다. 또한 테스트코드가 작성되기 힘들다면 나쁜냄새를 발견하고 개선할 수 있을 것입니다.


📕Reference

https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/html/boot-features-testing.html
https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/mock/mockito/MockBean.html
https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.spring-boot-applications.spring-mvc-tests
https://jojoldu.tistory.com/320

profile
열심히 공부중입니다😇

0개의 댓글