Mockito

seongmin·2022년 11월 16일
0

Spring

목록 보기
38/38
post-thumbnail

Mock 객체를 사용하여 슬라이스 테스트를 진행한다. Mock 객체를 이용하게 되면 다른 계층과 단절되어 불필요한 과정을 줄일 수 있다.

Controller

@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerMockTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    // (1)
    @MockBean
    private MemberService memberService;

    // (2)
    @Autowired
    private MemberMapper mapper;

    @Test
    void postMemberTest() throws Exception {
        // given
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
                                                        "홍길동",
                                                    "010-1234-5678");
				
        Member member = mapper.memberPostToMember(post);  // (3)
        member.setStamp(new Stamp());    // (4)

        // (5)
        given(memberService.createMember(Mockito.any(Member.class)))
                .willReturn(member);

        String content = gson.toJson(post);

        // when
        ResultActions actions =
                mockMvc.perform(
                                    post("/v11/members")
                                        .accept(MediaType.APPLICATION_JSON)
                                        .contentType(MediaType.APPLICATION_JSON)
                                        .content(content)
                                );

        // then
        MvcResult result = actions
                                .andExpect(status().isCreated())
                                .andExpect(jsonPath("$.data.email").value(post.getEmail()))
                                .andExpect(jsonPath("$.data.name").value(post.getName()))
                                .andExpect(jsonPath("$.data.phone").value(post.getPhone()))
                                .andReturn();

//        System.out.println(result.getResponse().getContentAsString());
    }
}
  1. @MockBean 애너테이션은 Application Context에 등록되어 있는 Bean에 대한 Mockito Mock 객체를 생성하고 주입해주는 역할을 한다.

    (1)과 같이 @MockBean 애너테이션을 필드에 추가하면 해당 필드의 Bean에 대한 Mock 객체를 생성한 후, 필드에 주입(DI)한다.

    (1)에서는 MemberService 빈에 대한 Mock 객체를 생성해서 memberService 필드에 주입한다.

  1. (2)에서 MemberMapper를 DI 받는 이유는 MockMemberService(가칭)의 createMember()에서 리턴하는 Member 객체를 생성하기 위함이다.

  2. (3)에서 MemberMapper를 이용해 post(MemberDto.Post 타입) 변수를 Member 객체로 변환하고 있다.

    MemberMapper를 굳이 사용하지 않고 new Member() 와 같이 Member 객체를 생성해도 되지만 여기서는 post 변수를 재사용하기 위해 MemberMapper로 변환을 했다.

  1. 실제 MemberService의 createMember()에서 회원 정보 등록 시, Stamp 정보도 등록되며, createMember() 의 리턴 값(Member 객체)에 Stamp 정보 포함이 됩니다.

    따라서 MockMemberService(가칭)의 createMember()에서도 리턴 값으로 Stamp 정보가 포함된 Member 객체를 리턴하도록 (4)와 같이 Stamp 객체를 추가해준다.

    만약 Stamp 객체를 추가해주지 않으면 MemberResponseDto 클래스 객체가 JSON으로 변환되는 과정 중에 Stamp에 대한 정보가 없다는 예외가 발생한다.

  2. (5)는 Mockito에서 지원하는 Stubbing 메서드다.

    given(memberService.createMember(Mockito.any(Member.class)))

    given() 은 Mock 객체가 특정 값을 리턴하는 동작을 지정하는데 사용하며, Mockito에서 지원하는 when()과 동일한 기능을 한다.

    여기서는 Mock 객체인 memberService 객체로 createMember() 메서드를 호출하도록 정의하고 있다.

    createMember()의 파라미터인 Mockito.any(Member.class) 는 Mockito에서 지원하는 변수 타입 중 하나다.

    MockMemberService(가칭)가 아닌 실제 MemberService 클래스에서 createMember()의 파라미터 타입은 Member 타입이다.

    따라서 Mockito.any() 에 Member.class를 타입으로 지정해 주었다.

    .willReturn(member)

    MockMemberService(가칭)의 createMember() 메서드가 리턴 할 Stub 데이터이다.

MockMemberService(가칭) 클래스는 우리가 테스트하고자 하는 Controller의 테스트에 집중할 수 있도록 다른 계층과의 연동을 끊어주는 역할을 한다.

Service

@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher publisher;

    public MemberService(MemberRepository memberRepository,
                         ApplicationEventPublisher publisher) {
        this.memberRepository = memberRepository;
        this.publisher = publisher;

    }

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());     // (1)
        Member savedMember = memberRepository.save(member);

        publisher.publishEvent(new MemberRegistrationApplicationEvent(this, savedMember));
        return savedMember;
    }

    ...
		...

    private void verifyExistsEmail(String email) {
        Optional<Member> member = memberRepository.findByEmail(email);  // (2)

        // (3)
        if (member.isPresent())
            throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
    }
}

테스트 하고자 하는 부분은 createMember() 메서드의 (1)과 같이 DB에 존재하는 이메일인지 여부를 검증하는 verifyExistsEmail() 메서드가 정상적인 동작을 수행하는지를 테스트하는 것이다.

그런데 verifyExistsEmail() 메서드의 내부를 보면 (2)와 같이 verifyExistsEmail() 메서드의 파라미터로 전달 받은 email을 조건으로 한 회원 정보가 있는지 memberRepository.findByEmail(email) 을 통해 DB에서 조회하고 있다.

하지만 verifyExistsEmail() 메서드가 DB에서 Member 객체를 잘 조회하는지 여부를 테스트 하려는게 아니라, 어디서 조회해 왔든 상관없이 조회된 Member 객체가 null 이면 BusinessLogicException 을 잘 던지는지 여부만 테스트하면 된다.

(3)과 같이 단 두 줄 짜리 코드이더라도 member 객체 null 여부를 판단하는 것 역시 엄연히 비즈니스 로직이라고 볼 수 있다.

따라서 DB에서 회원 정보를 조회하는 (2)의 memberRepository.findByEmail(email) 은 Mocking의 대상이 된다.

  • Mockito를 이용한 Mocking
// (1)
@ExtendWith(MockitoExtension.class)
public class MemberServiceMockTest {
    @Mock   // (2)
    private MemberRepository memberRepository;

    @InjectMocks    // (3)
    private MemberService memberService;

    @Test
    public void createMemberTest() {
        // given
        Member member = new Member("hgd@gmail.com", "홍길동", "010-1111-1111");

        // (4)
        given(memberRepository.findByEmail(member.getEmail()))
                .willReturn(Optional.of(member)); // (5)

				// when / then (6)
        assertThrows(BusinessLogicException.class, () -> memberService.createMember(member));
    }
}
  1. Spring을 사용하지 않고, Junit에서 Mockito의 기능을 사용하기 위해서는 (1)과 같이 @ExtendWith(MockitoExtension.class) 를 추가해야 한다.

  2. (2)와 같이 @Mock 애너테이션을 추가하면 해당 필드의 객체를 Mock 객체로 생성한다.

  3. (3)과 같이 @InjectMocks 애너테이션을 추가한 필드에 (2)에서 생성한 Mock 객체를 주입해 준다.
    즉, (3)의 memberService 객체는 주입 받은 memberRepository Mock 객체를 포함하고 있다.

  4. (4)에서는 (2)에서 생성한 memberRepository Mock 객체로 Stubbing을 하고 있다.

    memberRepository.findByEmail(Mockito.anyString()) 의 리턴 값으로 (5)와 같이 Optional.of(member) 를 지정했기 때문에 테스트 케이스를 실행하면 결과는 “passed” 이다.

Optional.of(member) 의 member 객체에 포함된 이메일 주소가 memberService.createMember(member) 에서 파라미터로 전달한 member 객체에 포함된 이메일 주소와 동일하기 때문에 검증 결과가 “passed” 이다.

0개의 댓글