@SpringBootTest 말고 Mockito 를 사용하면 어떨까 ?
Mockito 란?
Mockito 는 테스트 목적으로 Mock 개체를 생성하도록 특별히 설계된 Mocking 프레임워크이다.
Mocking은 단위 테스트 시 외부 종속성이 있는 경우 사용되는 프로세스로, 외부 종속성의 동작이나 상태가 아니라 테스트 중인 코드에 집중한다. 또한, 대상을 실제 개체가 아닌 Mock과 같은 모의(가짜) 개체로 대체한다.
Mockito를 사용하면 Mock 개체의 기대치와 동작을 정의하여 테스트 중에 특정 시나리오를 시뮬레이션할 수 있게 된다.
이를 통해, Junit을 보완할 수 있다.
주요 장점은 아래와 같다.
1. No handwriting : 개발자들이 직접 Mock 코드를 구현할 필요가 없다.
2. Annotation support : Annotation 을 지원하기 때문에, 쉽게 사용할 수 있다.
3. Safe Refactoring : 인터페이스 메서드 명이 바뀌거나 매개변수가 바뀌어도 테스트 코드를 변경할 필요가 없다.
Unit Tests with @SpringBootTest
Mockito를 사용했을 때와 비교해보기 위해 우선, Mockito를 사용하지않고 테스트 코드를 작성했다.
이전에 나는 @Autowired 와 같이 스프링에서 제공하는 Annotation을 사용하기 위해 @SpringBootTest를 사용했다.
예시로, unique 값으로 설정한 Member의 이메일로 Member 를 찾는 findByEmail() 을 테스트 해보자.
- 적용 코드
@SpringBootTest
class MemberServiceTest {
@Autowired
MemberRepository memberRepository;
@Autowired
MemberService memberService;
Member member;
@BeforeEach // @Test 이전에 실행
void setup() {
member = Member.builder()
.email("limnj@test.com")
.passwd("1234")
.name("홍길동")
.build();
}
@Test
@DisplayName("이메일로 회원 찾기 - 성공")
public void givenMemberObject_whenFindByEmail_thenReturnMemberObject() {
// given - precondition or setup
memberRepository.save(member);
// when - action or the behaviour that we are going test
Optional<Member> findMember = memberService.findByEmail(member.getEmail());
// then - verify the output
assertTrue(findMember.isPresent());
assertThat(findMember.get().getEmail()).isEqualTo(member.getEmail());
assertThat(findMember.get().getPasswd()).isEqualTo(member.getPasswd());
assertThat(findMember.get().getName()).isEqualTo(member.getName());
}
}
그런데 실제 개체를 이용하기 때문에,
운영 DB에 중복되는 데이터가 있다면 테스트 실패와 DataIntegrityViolationException 이 발생한다.
조건 : 이메일을 unique 값으로 하고 테스트 객체의 이메일을 갖는 Member를 미리 운영 DB 에 저장해두었으며, 따로 예외 처리는 하지 않았다.
여기서 고민을 했던 부분은,
1. 이렇게라도 예외 처리를 못한 것에 발견할 수 있어서 감사해야하는지
2. findByEmail() 의 단위 테스트라는 목적에 맞지 않은건지
였는데 일단 나는 실제 개체를 이용했을 때와 Mock 개체를 이용했을 때의 차이점을 보여주고싶었다.
Mockito를 사용하면 위의 예외가 발생하지 않기 때문이다.
Unit Tests with Mockito
Mockito를 사용하기 위해서는 @ExtendWith() 안에 MockitoExtension.class를 넣어줘야하는데, 여러 개 넣을 수도 있다.
하나씩 살펴보자.
1. 의존성 주입(Dependency Injection)
테스트 환경에서 아래의 @InjectMocks은 스프링의 @Autowired 와 같은 역할을 한다.
여기서 주의할 점은 Mockito에서 DI 대상은 인터페이스가 아니라 구현체여야 한다. ( MemberServiceImpl )
2. @Mock과 @InjectMocks
아래의 코드를 예를 들어, MemberService는 MemberRepository가 필요하다.
그렇기 때문에 @Mock 으로 MemberRepository를 Mock 개체로 만들고,
공식 문서에 따라 @InjectMocks는 @Spy 또는 @Mock 을 사용하여 생성된 mock/spy만 주입한다.
3. given().willReturn()
아래에서는 BDDMockito 의 given().. 을 사용하였는데, Mockito를 상속받아 when()을 호출하기 때문에 동작이 같다.
그래서 Mockito.when().. 에 대한 공식문서를 보면 'x 메서드가 호출되면, y를 반환한다' 고 한다.
즉, given(x).willReturn(y) 를 통해 x 메서드가 호출되면 y를 반환하게 설정이 가능하다.
- 적용 코드
@ExtendWith({MockitoExtension.class})
class MemberServiceTest {
@Mock
MemberRepository memberRepository; // memberRepository
@InjectMocks
MemberServiceImpl memberServiceImpl; // com.limnj.til.service.MemberServiceImpl@22c86919
Member member;
@BeforeEach // @Test 이전에 실행
void setup() {
member = Member.builder()
.email("limnj@test.com")
.passwd("1234")
.name("홍길동")
.build();
}
@Test
@DisplayName("이메일로 회원 찾기 - 성공")
public void givenMemberObject_whenFindByEmail_thenReturnMemberObject() {
// given - precondition or setup
given(memberRepository.findByEmail(member.getEmail())).willReturn(Optional.of(member));
// when - action or the behaviour that we are going test
Optional<Member> findMember = memberServiceImpl.findByEmail(member.getEmail());
// then - verify the output
assertTrue(findMember.isPresent());
findMember.ifPresent(
m -> {
assertThat(m.getEmail()).isEqualTo(member.getEmail());
assertThat(m.getPasswd()).isEqualTo(member.getPasswd());
assertThat(m.getName()).isEqualTo(member.getName());
}
);
}
}
정리
Mockito 를 통해 외부 종속성을 Mock 으로 대체하여 테스트 개발을 간단히 할 수 있다.
또한, SpringBootTest와 같이 모든 설정을 불러오지 않아도 되기 때문에 더 빠르다.
위의 예제 말고도 다른 예제들이 많기때문에 차근차근 정리해봐야겠다.
참고
https://www.javaguides.net/2022/03/spring-boot-unit-testing-service-layer.html
https://www.baeldung.com/junit-5-extensions
https://www.baeldung.com/mockito-junit-5-extension
https://stackshare.io/stackups/junit-vs-mockito
https://www.telerik.com/products/mocking/unit-testing.aspx
https://www.geeksforgeeks.org/difference-between-mock-and-injectmocks-in-mockito/
'Web' 카테고리의 다른 글
[Spring] compiletime 과 runtime 에 대해서 (0) | 2023.08.13 |
---|---|
[Spring] XSS Prevention (2) | 2023.07.31 |
[Spring] MapStruct 사용하기 (0) | 2023.07.08 |
[Spring] 라이브러리 버전 관리하기 ( Gradle ) (0) | 2023.07.06 |
[Spring] Exception Handling 에 대해서 (3) | 2023.07.03 |