본문 바로가기
공부/프로그래밍

[junit5] Mock을 이용한 단위 테스트 (@InjectMocks 과 @Mock 차이)

by demonic_ 2020. 8. 13.
반응형

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에 들어있는것을 확인할 수 있었다. 추후 외부 라이브러리를 참조할때 사용하면 테스트를 작성할 때 적절하게 사용할 수 있을 거 같다.

 

 

 

끝.

 

 

반응형

댓글