Mockito를 이용하면 좀더 작은 단위까지 테스트가 가능하다.
무엇보다 데이터를 컨트롤해야하는 상황에서 DB연결없이 임의로 주고받을 수 있기 때문에 유용하다.
이번에는 그 테스트에 관한 내용과 각 어노테이션의 사용법에 대해 나열한다.
그럼 한번 시작해보자.
테스트 편의성을 위해 Lombok을 사용했다.
다음과 같이 Member 엔터티가 있다.
@Entity
@Table(name = "Member")
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "status")
private MemberStatus status;
@Column(name = "reg_dt")
@CreationTimestamp
private LocalDateTime regDt;
public enum MemberStatus{
NORMAL, DORMANENT, DROP
}
@Builder
public Member(String name, Integer age, MemberStatus status, LocalDateTime regDt) {
this.name = name;
this.age = age;
this.status = status;
this.regDt = regDt;
}
}
그리고 Member를 조회하는 Repository를 만든다.
@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
public void persist(Member member) {
em.persist(member);
}
public Member findOne(Long memberId) {
return em.find(Member.class, memberId);
}
}
Member ID를 이용해 Member를 조회하는 서비스를 만든다
@Service
@RequiredArgsConstructor
public class TestService {
private final MemberRepository memberRepository;
@Transactional(readOnly = true)
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
여기서 Service를 테스트하기 위해서는 MemberRepository를 주입받아야 한다.
그런데 이것을 주입받으려면 스프링을 띄울 수 밖에 없고, 테스트를 하는데 시간이 오래걸린다.
무엇보다 데이터가 없는 경우 문제가 발생할 수 있다.
그래서 Mock를 이용해 테스트를 하면 간단하다
Junit4 에서는 @RunWith가 있는데 Junit5는 없다. 대신 @ExtendWith 라는 명령어를 대신 사용하면 된다.
@ExtendWith(SpringExtension.class)
class MemberRepositoryTest {
...
Junit4일 경우 다음과 같이 선언
@RunWith(MockitoJUnitRunner.class)
public class MemberRepositoryTest {
...
행여 잡히지 않는다면 다음 디펜던시가 있는지 확인하고 추가하자(junit5 기준)
dependencies {
...
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
...
}
테스트를 다음과 같이 작성한다.
...
@Mock
private TestService testService;
@Test
void saveTest() {
// given
Member saveMember = Member.builder()
.name("테스트")
.age(20)
.status(Member.MemberStatus.NORMAL)
.build();
when(testService.findOne(any())).thenReturn(saveMember);
// when
Member findMember = testService.findOne(1L);
// then
System.out.println("=============================");
System.out.println("findMember = " + findMember);
Assertions.assertThat(findMember.getStatus()).isEqualTo(saveMember.getStatus());
Assertions.assertThat(findMember.getName()).isEqualTo(saveMember.getName());
}
...
TestService는 MemberRepository를 주입받아야 한다. 그런데 이렇게 실행하면 테스트는 성공하는 대신에 TestService 내에 MemberRepository는 null 상태가 되어잇다. 객체주입을 하지 않았기 때문이다.
TestService안에 있는 memberReposity가 null 임을 확인할 수 있다.
그럼에도 불구하고 테스트는 통과되는데 해당메서드의 리턴값을 thenReturn을 이용해 지정해두었기 때문이다.
실질적으로 memberRepository의 기능을 사용한 것은 아무것도 없다.
# @Spy와 @Mock의 차이는 무엇일까
@Mock은 로직이 삭제된 빈 껍데기라고 보면 된다. 실제로 메서드는 갖고 있지만 내부 구현이 없는 상태이다.
@Spy는 모든 기능을 가지고 있는 완전한 객체다.
대체로 Spy보다는 Mock을 사용하길 권고한다. 하지만 외부라이브러리를 이용한 테스트에는 @Spy를 사용하는 것을 추천한다.
위의 예제를 통해 Spy를 사용했을때를 살펴보자
...
@Spy
private TestService testService;
@Test
void saveTest() {
// given
Member saveMember = Member.builder()
.name("테스트")
.age(20)
.status(Member.MemberStatus.NORMAL)
.build();
when(testService.findOne(any())).thenReturn(saveMember);
// when
Member findMember = testService.findOne(1L);
// then
System.out.println("=============================");
System.out.println("findMember = " + findMember);
Assertions.assertThat(findMember.getStatus()).isEqualTo(saveMember.getStatus());
Assertions.assertThat(findMember.getName()).isEqualTo(saveMember.getName());
}
...
org.mockito.exceptions.base.MockitoException: Unable to initialize @Spy annotated field 'testService'.
Please ensure that the type 'TestService' has a no-arg constructor.
위와같은 에러가 났다. testService를 생성할때 받아야할 인자를 받지 못했기 때문이다.
@Mock을 사용할때는 주입받아야 할 것이 없다 하더라도 빈 객체라도 생성되었는데, @Spy는 명확히 되어있지 않으면 에러를 발생시킨다.
# 테스트 객체의 각 항목을 주입받게 하려면?
@InjectMocks 라는 어노테이션이 있다. 이게 달리면 @Spy, @Mock 어노테이션이 붙은 객체들을 주입시켜준다.
다음 샘플을 보자.
...
@InjectMocks
private TestService testService;
@Test
void saveTest() {
// given
Member saveMember = Member.builder()
.name("테스트")
.age(20)
.status(Member.MemberStatus.NORMAL)
.build();
when(testService.findOne(any())).thenReturn(saveMember);
// when
Member findMember = testService.findOne(1L);
// then
System.out.println("=============================");
System.out.println("findMember = " + findMember);
Assertions.assertThat(findMember.getStatus()).isEqualTo(saveMember.getStatus());
Assertions.assertThat(findMember.getName()).isEqualTo(saveMember.getName());
}
...
테스트를 실행하면 다음과 같은 에러가 발생한다.
java.lang.NullPointerException
at kr.sample.demo.demo202008.TestService.findOne(TestService.java:16)
at kr.sample.demo.demo202008.MemberRepositoryTest202008.saveTest(MemberRepositoryTest202008.java:35)
...
여기서 null은 memberRepository를 지목한다. 주입을 받아야하는데 받지 못했기 때문이다
그래서 다음을 추가해준다.
...
@InjectMocks
private TestService testService;
//추가
@Mock
private MemberRepository memberRepository;
@Test
void saveTest() {
// given
Member saveMember = Member.builder()
.name("테스트")
.age(20)
.status(Member.MemberStatus.NORMAL)
.build();
when(testService.findOne(any())).thenReturn(saveMember);
// when
Member findMember = testService.findOne(1L);
// then
System.out.println("=============================");
System.out.println("findMember = " + findMember);
Assertions.assertThat(findMember.getStatus()).isEqualTo(saveMember.getStatus());
Assertions.assertThat(findMember.getName()).isEqualTo(saveMember.getName());
}
...
testService에서 Mockito로 감싸여 주입된것을 확인할 수 있다.
테스트 역시 통과된다.
위사례의 경우 memberRepository 역시 스프링 기능에 많이 의존적이기 때문에 테스트에 적절하진 않지만, testService에 들어있는것을 확인할 수 있었다. 추후 외부 라이브러리를 참조할때 사용하면 테스트를 작성할 때 적절하게 사용할 수 있을 거 같다.
끝.
'공부 > 프로그래밍' 카테고리의 다른 글
[react] hook 에서 componentDidMount, componentWillUnmount 기능 구현하기 (0) | 2020.08.21 |
---|---|
[springboot] @valid 테스트하기(Controller에 입력되는 객체 테스트) (0) | 2020.08.18 |
[gradle] 외부 jar파일 추가하기 (1) | 2020.08.11 |
[springboot] ControllerAdvice를 이용해 정해진 폼으로 리턴하기 (0) | 2020.08.06 |
[aws] Jenkins + CodeDeploy 로 로드밸런스 환경 자동배포하기 (0) | 2020.08.04 |
댓글