티스토리 뷰

728x90
반응형

지난 글에서 API 계층에 Slice Test를 진행하면서

 

테스트를 위해 DB 계층을 포함한 앱 전체의 로직이 실행되기 때문에 엄밀한 의미에서 Slice Test는 아니라고 언급했었다.

 

2022.09.08 - [개발/Spring] - [Spring]Slice Test - API Layer

 

[Spring]Slice Test - API Layer

지난 글까지 테스트의 종류를 알아보고, 가장 작은 범위인 단위 테스트를 비즈니스 로직에 적용해 봤다. 2022.09.07 - [개발/Spring] - [Spring]단위 테스트(Unit Test) 2022.09.07 - [개발/Spring] - [Spring]JUn..

gnidinger.tistory.com

이 문제는 Mock 객체를 도입하면 해결되는데, 이번 글에서는 그 방법에 대해 살핀다.

 

Mock

 

Mock은 직역하면 모조품이라는 뜻을 가지고 있다.

 

제품의 개발 단계에서 내부적으로 테스트하기 위한, 실물 크기의 모형을 Mock-up이라 부르는 것과 비슷하다.

 

개발자에게 있어 Mock은 가짜(모조품) 객체를 의미하며, 테스트에 Mock 객체를 사용하는 것을 Mocking이라 부르기도 한다.

 

그렇다면 굳이 가짜 객체를 만들어서 테스트를 진행하는 이유는 무엇일까.

 

Slice Testing Workflow without a Mock object

 

먼저 Mock 객체를 사용하지 않은 테스트의 Workflow를 확인하자.

 

지난 글에서 작성했던 테스트 메서드의 실행 흐름이다.

 

물론 잘 작동하는 것을 확인 했지만, postMember() 테스트를 위해 DB까지 흐름이 이어지기 때문에

 

슬라이스 테스트라기 보다는 통합 테스트에 가까운 모습을 보이고 있다.

 

슬라이스 테스트의 목적해당 계층 테스트에 집중하는 것임을 감안하면 좋은 흐름은 아니라고 할 수 있다.

 

테스트 단위는 작을 수록 좋기 때문이다.

 

Slice Testing Workflow with a Mock object

 

계속해서 Mock 객체를 도입해 슬라이스 테스트를 다른 테스트로부터 독립시켜보자.

 

위 그림은 같은 테스트에 Mock 객체를 사용한 실행 흐름이다.

 

postMember()의 요청이 서비스 계층까지 내려가지 않고 MockMemberService의 메서드를 호출하는 것을 볼 수 있다.

 

따라서 다른 계층과의 연결이 끊어진 독립적인 테스트를 진행할 수 있으며, 실행 단계도 절반으로 줄어들었다.

 

이제 진정한 의미의 Slice Test를 진행할 수 있게 된 것이다.

 

Mockito

 

Mockito는 위와 같은 Mock 객체를 생성하고 관리하는 Mocking Library 중 하나이다.

 

물론 비슷한 기능을 하는 라이브러리가 많이 있지만,

 

Mockito는 Spring Framework 자체적으로도 지원하고 있으며 사용자 또한 가장 많다.

 

무엇보다 테스트시 검증 순서를 보장하며(Order Support)

 

애너테이션을 사용한 간편한 Mock 객체 생성(Annotation Support)과 그에 따른 테스트와 타 계층 단절,

 

그리고 예외 처리도 다룰 수 있다는(Exception Support)점은 Mockito의 큰 강점으로 작용한다.

 

계속해서 작성해온 테스트에 Mockito를 적용해보자.

 

Slice Tests with Mockito

 

MemberControllerMockTest

 

import com.gnidinger.member.dto.MemberDto;
import com.gnidinger.member.entity.Member;
import com.gnidinger.member.mapper.MemberMapper;
import com.gnidinger.member.service.MemberService;
import com.gnidinger.stamp.Stamp;
import com.google.gson.Gson;
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 static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

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

    @Autowired
    private Gson gson;

    @MockBean // Container에 등록된 Bean(MemberService)에 대한 Mock Object 생성 및 (memberService)에 주입
    private MemberService memberService;

    @Autowired // MockMemberService의 createMember()에서 리턴하는 Member Object 생성이 목적
    private MemberMapper mapper;

    @Test
    void postMemberTest() throws Exception {
        // given
        MemberDto.Post post = new MemberDto.Post("gni@gmail.com", "거니", "010-1234-5678");

        Member member = mapper.memberPostToMember(post); // post 재사용을 위해 MemberMapper로 post -> Member Object 변환
        member.setStamp(new Stamp()); // MockMemberService의 createMember()에서 리턴하는 Member에 Stamp 정보 포함

        // given() - Mockito의 Stubbing Method. Mock 객체가 특정 값을 리턴하는 동작 지정. when()과 동일한 역할을 함
        // memberService(Mock Object)의 createMember()를 호출하도록 설정
        // Mockito.any(Member.class)는 Class를 매개변수로 받는 메서드
        // 실제 MemberService의 createMember()의 파라미터인 Member.class를 변수로 지정
        // willReturn(member) - MockMemberService의 createMember()가 리턴할 Stub Data 지정
        given(memberService.createMember(Mockito.any(Member.class))).willReturn(member);

        String content = gson.toJson(post);

...
    }
}

지난 Slice Test 글에서 작성했던 테스트 클래스에 Mockito를 적용했다.

 

위와 같이 작성한 뒤 MemberController 클래스의 postMember() 안에 BreakPoint를 걸고

 

MockTest 클래스를 디버그 모드로 실행하면

 

위와 같이 테스트 메서드에서 호출하는 memberService 객체가 Mock 객체인 것을 확인할 수 있다.

 

MemberServiceMockTest

 

계속해서 서비스 클래스의 비즈니스 로직인 createMember() 메서드 테스트에 Mockito를 적용해보자.

 

구체적으로 테스트를 진행할 부분은 아래와 같다.

...

public class MemberService {
    ...

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail()); // 테스트 타겟
        Member savedMember = memberRepository.save(member);

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

    ...

    private void verifyExistsEmail(String email) {
        Optional<Member> member = memberRepository.findByEmail(email); // 회원 정보를 위해 DB 조회(개선 부분, Mocking 대상)
        
        if (member.isPresent()) // 검증 로직
            throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
    }
}

주석에 적은 대로 지금은 메서드 동작시 DB까지 내려가 조회를 하고 있다.

 

테스트의 관심사는 Member 데이터의 출처가 아니라 객체가 null일 경우 예외를 던지는지의 여부이기에,

 

DB까지 내려가 정보를 조회하는 findByEmail()메서드는 Mocking 대상이 된다.

@ExtendWith(MockitoExtension.class) // Spring 없이 JUnit에서 Mockito 사용하기 위해 추가
public class MemberServiceMockTest {
    @Mock // 해당 필드의 Object를 Mock Object로 생성
    private MemberRepository memberRepository;

    @InjectMocks // 추가된 필드에 @Mock으로 생성한 Mock Object(memberRepository Mock Object) 주입
    private MemberService memberService;

    @Test
    public void createMemberTest() {
        // given
        Member member = new Member("gni@gmail.com", "gni", "010-1234-5678");

        // memberRepository Mock 객체로 Stubbing
        given(memberRepository.findByEmail(Mockito.anyString()))
                .willReturn(Optional.of(member)); // 리턴 값으로 Optional.of(member) 지정 -> 테스트 pass

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

위 코드는 Mockito를 적용한 테스트 클래스이다.

 

Mock 객체를 생성하고 주입, Stubbing에 이용하는 것을 쉽게 확인할 수 있다.

 

given()의 매개변수와 상관없이 .willReturn()으로 출력 값을 정하기 때문

 

Optional.of(member)의 정보와 memberService.createMember(member)의 정보가 같아

 

테스트에 통과하는 것도 볼 수 있다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함