테스트

[테스트] 테스트를 위한 Mock

leegeonwoo 2024. 9. 1. 21:22

이메일을 전송하는 비즈니스 로직의 테스트코드를 작성해야한다고 해보자
어떻게 작성해야할까?

'벤더의 SMTP를 통하여 클라이언트로부터 받은 정보를 데이터로 만들어 이메일을 전송한다'라는 로직을 수행해야하는데 일반적으로 테스트코드를 작성한다면 실제 이메일이 테스트코드를 실행할때마다 날라가게된다. 만약 회원가입시에 이메일을 통해 인증번호를 받고 인증번호를 서버와 일치하는지 확인하고.. 이런테스트 코드를 작성하려면 너무 많은 고민을 해야하고 시간과 자원을 낭비하게 될 수 있다.

"만약 이메일을 전송한셈 치면어떨까?" 라는 생각으로 사용하는 테스트 라이브러리가 Mock이다.
Mock은 "가짜"라는 의미로 테스트 코드에서 사용하고자하는 컴포넌트를 가짜 컴포넌트 대상으로 만들어 해당 컴포넌트에 대한 반환값 또는 동작 기능을 테스트코드 내에서 임의로 설정할 수 있다.
이러한 편리한 특징을 가지고있을 뿐 아니라 Mock을 사용하면 실제 개발 코드에서는 스프링과 같은 프레임워크에 의존하더라도 테스트코드에서는 독립성을 갖도록 하여 단위 테스트를 원활하게 수행할 수 있도록 도와준다.

만약 스프링 프레임워크를 사용하여 테스트 코드를 짠다면 테스트를 위한 의존성 설정은 아래의 코드와 같을 것이다.

@SpringBootTest  
class MailServiceTestBySpring {  

    @Autowired  
    MailSendClient mailSendClient;  

    @Autowired  
    MailSendHistoryRepository mailSendHistoryRepository;  

    @Autowired  
    MailService mailService;  

    @Test  
    @DisplayName("test")  
    void test() {  
        // given  
        assertThatThrownBy(() -> mailSendClient.sendEmail("", "", "", "")).isInstanceOf(IllegalArgumentException.class);  

        //when  

        //then    }  
}

@SpringBootTest를 사용하여 컴포넌트에 의존성을 불러올 준비를 하고,
@Autowired를 통해 해당 컴포넌트에 의존성을 부여해주어야한다.
이 과정에서 자연스레 단위 테스트가 어려워지게 된다.

이제 Mock을 어떻게 사용하는지 보자

class MailServiceTest {  

    @Test  
    @DisplayName("메일 전송 테스트")  
    void sendMail() {  
        // given  
        MailSendClient mailSendClient = mock(MailSendClient.class);  
        MailSendHistoryRepository mailSendHistoryRepository = mock(MailSendHistoryRepository.class);  
        MailService mailService = new MailService(mailSendClient, mailSendHistoryRepository);  

        when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))  
                .thenReturn(true);  

        //when  
        boolean result = mailService.sendMail("", "", "", "");  

        //then  
        assertThat(result).isTrue();  
    }

given절에서 각각의 컴포넌트에 mock메서드를 통하여 가짜 의존성(?)을 주입해주는 것을 확인할 수 있다

MailSendClient mailSendClient = mock(MailSendClient.class);
mailSendClient를 선언해줌과 동시에 실제 사용하는 객체인 MailSendClient.class의존성을 부여해주지만 해당 객체를 임의로 선언해준다.

when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))  
        .thenReturn(true);

Mock객체를 사용하기위해서는 해당 객체가 개발과정에서 정의된 A라는 메서드의 동작을 했을때, B라는 동작이 나온다는 것을 임의로 정의해주어야한다.
정의 해줄때는 when메서드를 사용하여 정의해주고 그에 대한 반환값으로 thenReturn등의 메서드를 체이닝 방식으로 설정해준다.

anyString()은 Mock에서 제공해주는 편의메서드로 어떤 문자열이든 들어온다는 의미를 갖는다.

Mock선언을 어노테이션으로 편리하게하기

테스트 메서드 내부에서 Mock객체를 하나씩 선언해주는 것은 가독성이 떨어질 뿐만아니라 복잡하다.

Mock객체의 주입을 스프링의 @Autowired와 유사하게 할 수 있는 어노테이션을 제공해준다.

@Mock  
MailSendClient mailSendClient;  

@Mock  
MailSendHistoryRepository mailSendHistoryRepository;  

@InjectMocks  
MailService mailService;

@InjectMocks은 실제로 내가 테스트하고자하는 컴포넌트에 붙혀주면된다.

전체 코드를 살펴보면 아래와 같은데 무언가 어색한 부분이 있다.

@ExtendWith(MockitoExtension.class)  
class MailServiceTest {  

    @Mock  
    MailSendClient mailSendClient;  

    @Mock  
    MailSendHistoryRepository mailSendHistoryRepository;  

    @InjectMocks  
    MailService mailService;  

    @Test  
    @DisplayName("메일 전송 테스트")  
    void sendMail() {  
        // given  
        when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))  
                .thenReturn(true);  

        //when  
        boolean result = mailService.sendMail("", "", "", "");  

        //then  
        assertThat(result).isTrue();  

        verify(mailSendHistoryRepository, times(1))  
                .save(any(MailSendHistory.class));  
    }

바로 //given, //when, //then을 사용하여 테스트 역할의 구분을 명시하고 있는데 Mock을 사용하니 given절이 없고 given절안에 when이라는 메서드가 사용되고 있어 혼란을 야기한다.

그런 점을 보완하기위해 BDDMockito라는 기능을 제공하여 해당 경계를 명확하게 나타내도록하여 가독성을 높혀준다. when메서드가 given으로 바뀌고, thenReturn부분이 willReturn으로 바뀌는 등의 문법 차이가 있으니 주의하자.

BDDMockito.given(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))  
        .willReturn(true);
728x90