드이어 JUnit 관련해서 마지막 시간이다.
테스트 프로그램을 작성하는 연습도 틈틈히 해둬야
까먹지 않고 계층별 테스트가 원활하게 잘 될 것 같다.

새로운걸 알면알수록 재미있지만
내 뇌용량의 초과로,,, 전에 배웠던 내용들이 슬슬
휘발되기 시작한다 ㅋㅋ…

복습 하는시간도 마련해야 할 것 같은 느낌이다


어제는 계층별 테스트에 대해서 간단하게 포스팅 해보았다.
사실 계층별 테스트라고 작성을 했지만
진정한 Slice 테스트가 되는 프로그램이 아니였고
계층별 테스트를 위한 문법을 사전에 공부한느 시간이었다.

오늘은 정말 애플리케이션 계층별로 테스트를
진행하려면 어떻게해야하는지?

테스팅 기술에 대해 배워보는 시간이다.

Mockito

오늘 공부할 api는 Mockito라는 테스트를 위한 api이다.

우리가 평상시에 Mock이라고하면 가짜를 얘기한다.
목업폰과 같이 실제 기능은 없지는 모양만있는 가짜이다.

이와 같은 기술로 객체를 가짜로 만들어 계층별 테스트하는 방법이다.

너무나게 당연하게도 다른 계층을 접근하지 않으니
테스트시간이 확실히 빨라지게되며
계층별로 테스트를 해볼 수 있다는 장점이 있다.

Controller 계층 Slice Test

image

간단하게 Controller 계층만 두고 얘기해보자
어제 JUnit 2 챕터에서 테스트해보았던건
왼쪽에서 보는 것과 같은 형태의 ‘통합 테스트’라고 볼 수 있다.

이유는 핸들러메서드를 호출해 Service -> Repository 까지
접근해 Controller 계층이 잘 실행되는지를 알아보았기 때문이다.

이제 우리가 해볼거는 Mock 객체를 만들어
Test를 호출하면 Controller 계층만 독단적으로
테스트할 수 있는 방법이다.

@Transactional
@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerHomeworkTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @MockBean // 추가부분
    private MemberService memberService;

    @Autowired // mppaer 사용을 위한 추가
    private MemberMapper mapper;

    @Test
    void postMemberTest() throws Exception {
        ...
    }
}

코드를 예제로 살펴보자

이전과 달라진점은 @MockBean이 추가되었다는 점이다.
SpringBoot를 사용하는 어노테이션을 달아주었기때문에

@MockBean을 필드변수에 붙여주면
해당 객체에 Bean과 같은 타입의 객체가 DI된다.

즉 MemberService 클래스 객체가 주입되므로
우리는 가짜로 만들어진 memberService 객체로 메서드들을 호출할 수 있다.

@Test 프로그램을 작성해보자

@Test
void postMemberTest() throws Exception {
    MemberDto.Post post = new MemberDto.Post(
            "dhfif718@naver.com",
            "이재혁",
            "010-1234-5678"
    );

    Member member = mapper.memberPostToMember(post);
    member.setStamp(new Stamp());

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

    String content = gson.toJson(post);

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

    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();
}

대부분은 자바 문법적인 요소이고
중요한 부분은 given(); api 쪽이다.

import static org.mockito.BDDMockito.given;

해당 라이브러리를 import 해야 사용이 가능하고

given(); 메서드를 통해서 우리는 가짜 객체를 만들 수 있다.

given() : 가짜로 만들 메서드 지정
willRetrun() : 가짜로 만든 반환 값 지정
이렇게 생각하면 간단 할 것 같다.

여기서 @MockBean을 통해 의존성 주입받은 객체의
memberSerive.createMember()를 given()안에 넣어주었고
해당 .createMember()의 매개변수가 Member.class 타입이므로
Mockito.any(Member.class) 라고 설정을 해두었다.

여기서 만약 다른 타입일 경우에
Mockito.anyInt()
Mockito.anyLong()
Mockito.anyString()
등 여러가지 메서드들도 제공하고 있다.


테스트 코드를 이렇게 작성하면
가짜객체로 리턴값을 우리가 정해서 넘겨줄 수 있다.
Controller 계층 프로그램을 봐보자

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

    Member createdMember = memberService.createMember(member);

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

위에서 보듯이 memberService.createMember(member);
메서드를 호출하는 시점에서 우리의 가짜 객체의 반환값이
대신 createdMember 객체로 할당되게 되어진다.

그렇게 되면 Service 계층을 접근하지않고
우리가 만든 Mock으로만으로 Controller를 테스트 해 볼 수 있는 것이다.


Service 계층 Slice Test

Controller 계층을 테스트할 때에는 SpringBootTest를 이용했다.
스프링 부트를 실행시키는 시간도 포함하면
시간이 느려질 수 있는 단점이 있지만 Controller 계층은
spring web servlet 기술을 사용하기에 포함시켰었다.

하지만 Service 계층은 SpringBoot 없이도
Mockito API를 이용해 Serivce 계층만 테스트가 가능하다.

image

위와 같이 테스트가 이루어지고
Repository 계층과의 연결도 끊고 Mock 객체로만
테스트를 할 수 있다는 점이 중요하다.

@ExtendWith(MockitoExtension.class)
public class MemberServiceMockTest {

    @Mock
    private MemberRepository memberRepository;


    @InjectMocks
    private MemberService memberService;

    @Test
    void createMemberTest()  {
        ...
    }
}

Controller과는 다르게 @ExtendWith 어노테이션을 이용해
Mockito API를 사용할 수 있다.

여기서 우리가 테스트할 계층은 memberService 계층이고
가짜로 만들어야할 객체는 memberRepository이다

그런 개념에서 @Mock을 memberRepository에 붙이고
@InjectMocks를 memberService에 붙여주면

@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    ...

Service 계층의 Repository에 Mock객체가
자동으로 의존성 주입이된다.

그리하여 우리가 Repository를 Mock 객체를 사용할 수 있는 것이다.


이제 테스트 예제코드를 봐보자

@Test
void createMemberTest()  {
        Member member = new Member("dhfif718@naver.com","이재혁","010-1234-5678");

        given(memberRepository.findByEmail(member.getEmail())).willReturn(Optional.of(member));
        //given(memberRepository.findByEmail(Mockito.anyString())).willReturn(Optional.of(member));

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

검증을 어떻게하는지에 따라 달리지겠지만
Member 객체를 생성하는 serive 클래스 메서드를 테스트하기위해
새로운 Member 객체를 만들어 검증할 것이다.

given()으로 Controller 계층에서 했던것과 동일하게
가짜로만들 메서드의 형식과 매개타입을 입력해준다음
리턴값으로 어떤 객체를 받을지 정해주면된다.

최종적으로 동일한 Member가 발견되어
Exception을 잡아 검증하는 프로그램으로 작성하였다.

우리가 잘라줘야하는부분은 Repositroy의 findByEmail();로 정의한 부분이다.

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);
}

MemberService 로직은 너무 간단하게
기존 member와 email을 비교해 같은 email이 존재하면
throw Exception을 하게 되어있다.

우리는 위에서 메서드 호출할때의 매개변수 “dhfif718@naver.com”과
결과로 반환된 객체의 email “dhfif718@naver.com” 결과적으로 같기 때문에
if(member.isPresent()) 조건에 성립되
Exception을 던지게되어 Serivce 로직을 검증해볼 수 있다.

자연스럽게 예외가 던져지므로 해당 Service 로직은 종료되고
.save()까지 검증할 필요가 없어진다.
save를 검증하는 것은 사실 비지니스로직을 검증하는 것보단
Repository를 검증하는 것에 가깝기 때문에
Serivce 계층에서 검증하는 로직을 굳이 작성하지 않아도 될 것 같다.


오늘 만난 에러

이렇게 위에서 Controller과 Service 계층을 분리해서
따로 따로 테스트할 수 있게 프로그램을 작성하고
정상적으로 수행되는 모습을 볼 수 있었다.

Mockito라는 API를 처음 사용하다보니
여러가지 에러들을 만났고 그중에 기억남는 에러를 적어본다.

@Test
public void cancelOrderTest(){
    Order order = new Order();
    long orderId = 1L;
    Optional<Order> findOrder = Optional.of(order);
    given(orderRepository.findById(order.getOrderId())).willReturn(findOrder);
    assertThrows(BusinessLogicException.class, () -> orderService.cancelOrder(orderId));
}

위와 같이 테스트 코드를 작성후 실행시켜 보았는데

image

misusing.PotentialStubbingProblem 이라는 에러를 만났다.
잠재적인 Stubbing 문제라면서 예외가 잡혔다.

코드를 열심히 보다보니..
given()안의 매개변수로 order.getOrderId()를 넣어주고 있었고
cancelOrder();메서드 호출하는 매개변수에는 orderId를 넣어주고 있었다.

여기서 order객체가 만들어질때 Long타입으로 null로 만들어졌고
타입이 잘못들어간 상태로 테스트 코드를 실행하니까 위와 같은 에러가 발생했다.

해결은

given(orderRepository.findById(Mockito.anyLong())).willReturn(findOrder);

위와 같이 타입을 Mockito.anyLong()을 붙여서 해결이 가능하고
order.serOrderId(orderId);를 통해서 값을 넣어주어도
문제가 해결이 가능하다.


오늘 공부는 여기까지 했고
JUnit관련해서 Slice 테스트에 대해 자세히 배울 수 있어서
너무 유익한 시간이였다.

한가지 아쉬운점은 많은 api중에서도 한정적인 것만 사용해보았기에
지식층이 얕은점..?이 조금 아쉬웠고
이후에 프로젝트하기전에 테스트 관련 여러 메서드들을 공부한다음
테스트 코드를 작성해봐야겠다.

오늘 공부는 여기서 끝 !


오늘의 커피량: ☕️ ☕️ ☕️
오늘의 점심: 카레, 라면, 밥