GrowMe

Mockito와 Mocking 본문

Testing skills

Mockito와 Mocking

오늘도 타는중 2022. 8. 19. 07:46
Mockito와 Mocking에 대하여
# Mock
# Mock을 사용하는 이유
# Mockito
# Mockito를 활용한 Test (Controller)
# Mockito를 활용한 Test (Service)

*Mock이란?

  • 일을 할 때 쓰이는 목업의 의미는 실 제품 출시 전, 내부적으로 사용하기 위한 모형(가짜) 제품
  • 영어사전에서의 Mock의 의미
    not real, but intended to be very similar to a real situation, substance etc
    -> 즉, 가짜지만 진짜와 유사한 상황을 만들거나 진짜같은 물질 그자체를 의미
  • 개발에서의 Mock의 의미는 가짜 객체를 의미
  • Mocking : 단위 테스트 시, 테스트하고자하는 단위 내 코드 중 의존중인 객체를 가짜로 대체하여 작동하게하는 기술
    ex) Controller 테스트 시, Service를 주입받아 의존하여 사용 중일 때, 테스트는 Controller만 하고싶으므로 Service는 가짜로 대체해 동작하게 만들어 Real로 Controller만 Test가 가능하다. -> 이는 테스트에서 Mock 객체를 사용하는 이유가 된다.

[Test]                              Mock 객체 사용 X                                                                                           Mock 객체 사용

 


*Mokito

  • 단위 테스트를 위한 Java Mocking Framework
  • Junit위에서 동작하며, Mocking을 통해 원하는 단위만 잘라내어 테스트하는 것을 도와준다.

*의존성 추가

// Gradle의 경우
// https://mvnrepository.com/artifact/org.mockito/mockito-all
testImplementation group: 'org.mockito', name: 'mockito-all', version: '1.10.19'

// maven의 경우
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.10.19</version>
    <scope>test</scope>
</dependency>
  • SpringBoot를 사용 중이면, Gradle에 spring-boot-starter-test를 추가하게 되는데, 그 안에 mockito-core, mockito-junit-jupiter 패키지가 포함되어있다. 따라서 의존성을 따로 추가해주지 않아도 될 것이다.

*Mockito를 적용해 Controller 테스트하기

- MemberController

@RestController
@RequestMapping("/v11/members")
@Validated
public class MemberController {
    private final MemberService memberService;
    private final MemberMapper mapper;

    public MemberController(MemberService memberService, MemberMapper mapper) {
        this.memberService = memberService;
        this.mapper = mapper;
    }

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post requestBody) {
        Member member = mapper.memberPostToMember(requestBody);
        member.setStamp(new Stamp());

        Member createdMember = memberService.createMember(member);

        return new ResponseEntity<>(
                new SingleResponseDto<>(mapper.memberToMemberResponse(createdMember)),
                HttpStatus.CREATED);
    }
}

- MemberService

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

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

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

        return savedMember;
    }
    
    private void verifyExistsEmail(String email) {
        Optional<Member> member = memberRepository.findByEmail(email);
        if (member.isPresent())
            throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
    }
}

- MemberMapper

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MemberMapper {
    Member memberPostToMember(MemberDto.Post requestBody);
    Member memberPatchToMember(MemberDto.Patch requestBody);
    MemberDto.Response memberToMemberResponse(Member member);
    List<MemberDto.Response> membersToMemberResponses(List<Member> members);
}

- MemberControllerTest

import com.codestates.member.dto.MemberDto;
import com.codestates.member.entity.Member;
import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.service.MemberService;
import com.codestates.stamp.Stamp;
import com.google.gson.Gson;
import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willReturn;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @MockBean
    private MemberService memberService;

    @MockBean
    private MemberMapper mapper;

    @Test
    void postMemberTest() throws Exception {
        // given
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com","홍길동", "010-1111-1111");
        MemberDto.Response responseBody = new MemberDto.Response(1L,
                                                            "hgd@gmail.com",
                                                            "홍길동",
                                                            "010-1111-1111",
                                                                Member.MemberStatus.MEMBER_ACTIVE,
                                                                new Stamp());

        // Stubbing by Mockito
        given(mapper.memberPostToMember(Mockito.any(MemberDto.Post.class))).willReturn(new Member());

        given(memberService.createMember(Mockito.any(Member.class))).willReturn(new Member());

        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseBody);


        String content = gson.toJson(post);
        URI uri = UriComponentsBuilder.newInstance().path("/v11/members").build().toUri();

        // when
        ResultActions actions =
                mockMvc.perform(
                        MockMvcRequestBuilders
                                .post(uri)
                                .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());
    }
}
  • @MockBean : 스프링 컨텍스트에 선언한 객체를 Mock 객체(가짜 객체)로 등록한다. @SpringBootTest를 통해 애플리케이션이 실행되어 스프링 컨텍스트가 사용될 때, MemberController에 @Autowired를 통해 주입된 객체 대신 등록한Mock객체가 사용되게끔 동작된다.
  • given() : Mock 객체의 메서드가 특정 값을 리턴해주게끔 동작시키는 메서드이다. 테스트할 MemberController의 postMember 메서드에서는 mock 객체를 3번 사용하므로 given()도 3번 사용해주어 최종 리턴할 때 까지 가짜로 실행되게끔 동작시킨다.
  • Mockito.any() : Mock 객체 사용시 인자로 넣을 가짜 인자이다. Mockito.any()의 인자로 특정 클래스 타입을 지정하여 매개변수 타입을 일치시키는 용도로 사용한다. Mockito.anyInt(), Mockito.anyList() 등, 타입이 미리 정해진 메서드들도 존재한다.
  • willReturn() : given() 메서드가 반환할 값을 정해준다. 즉, given() 메서드는 항상 willReturn()의 인자로 넣어준 값을 반환한다. 이처럼 Mock 객체가 항상 일정한 동작을 하도록 만드는 것을 Stubbing이라고 하며, 이렇게 동작되는데 사용하는 코드 내의 responseBody와 같은 데이터를 Stub data라고 부른다.
  • MockMvc : 컨트롤러 테스트 시, 실제 서버에 구현한 애플리케이션을 올리지 않고(실제 서블릿 컨테이너를 사용하지 않고) 테스트용으로 시뮬레이션(가짜로 실행)하여 MVC 패턴으로 테스트가 되도록 도와주는 유틸 클래스이다. 해당 클래스에 대해서는 추후 자세히 포스팅할 예정이다...

- 테스트 결과

실제 repository 단까지 연결하여 테스트함과 유사하게, 테스트가 실행됨을 알 수 있다. 주석 처리를 풀어 확인해보면, 위처럼 응답 데이터가 내가 willReturn()에 넣어준 responseBody 데이터의 내용과 동일함을 확인 가능하다.


*Mockito를 적용해 Service 테스트하기

- MemberService

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

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

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

        return savedMember;
    }
    
    private void verifyExistsEmail(String email) {
        Optional<Member> member = memberRepository.findByEmail(email);
        if (member.isPresent())
            throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
    }
}

- MemberRepository

public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
}

- MemberServiceTest

@ExtendWith(MockitoExtension.class)
public class MemberServiceMockTest {
    @Mock
    private MemberRepository memberRepository;

    @InjectMocks
    private MemberService memberService;

    @Test
    public void createMemberTest() {
        // given
        Member member = new Member("hgd@gmail.com", "홍길동", "010-1111-1111");
        given(memberRepository.findByEmail(Mockito.anyString()))
                .willReturn(Optional.of(member));

        // when / then
        assertThrows(BusinessLogicException.class, () -> memberService.createMember(member));

    }
}
  • @ExtendWith(MockitoExtension.class) : Spring을 사용하지 않을 때(스프링 컨텍스트 사용 X) Mockito를 쓰기 위해서는 해당 어노테이션을 통해 Mockito 기능을 확장해주어야 한다.
  • @InjectMocks : 선언한 클래스가 필요한 의존 객체(Mock 객체)들을 감지(@Mock이 선언된 객체들과 비교)하여 @Mock으로 선언된 객체들을 사용해, @InjectMocks로 선언된 가짜 객체를 만듭니다.
  • @Mock : @InjectMocks로 선언된 클래스에서만 정의된 객체를 찾아서 의존성을 해결합니다.
    @MockBean과의 차이점은 @MockBean은 @SpringBootTest를 통해 사용되는 스프링 컨텍스트에 Mock 객체를 등록하지만, @Mock은 @InjectMocks로 Mock 객체가 만들어질때 의존하고 있는 Mock 객체를 함께 생성할 때 같이 사용됩니다. -> 즉, @MockBean / @SpringBootTest,   @Mock / @InjectMocks 이렇게 각각 짝꿍으로 사용됩니다.

- 테스트 결과

- 왜 여기선 given()이 한번 사용되었나요?

MemberService에서는 분명 Mock 객체인 MemberRepository가 두 번 사용되었음에도 Test 시에는 given()이 한번 사용되었고, 정상적으로 Test가 동작됩니다. 그 이유는 MemberService에서 MemberRepository를 두번째 사용될 때까지 도달하지 않기 때문입니다. 위 Test 로직은 verifyExistsEmail() 내에 존재하는 findByEmail()에서 일부러 Exception을 발생시켰기 때문에 MemberRepository를 두번째로 사용중인 memberRepository.save(member) 코드까지 도달하지 않습니다. 따라서, given()은 해당 테스트 내에서는 한번만 사용해도 정상적으로 테스트가 동작됩니다.

'Testing skills' 카테고리의 다른 글

스프링부트에서 JUnit 사용하기  (0) 2022.07.19
Comments