본문 바로가기
프로젝트

GiftFunding) Service 테스트 코드 작성하기

by son_i 2024. 4. 17.
728x90

테스트를 만들 서비스 클래스에서 Ctrl + Shift + T 를 눌러서 테스트 파일을 생성한다.

 

 

컨트롤러 테스트에서는 상단에 @WebMvcTest(XxxController.class)를 붙여주었고

@Autowired를 이용해 Mockmvc나 ObjectMapper를 주입받았다.

이것들은 이미 만들어진 Bean을 주입받아서 사용한다.

-> @WebMvcTest가 MockMvc를 빈으로 등록해줘서 @Autowired를 통해사용이 가능한 것.

 

* Autowired
스프링 컨테이너에 등록한 빈에게 의존관계주이이 필요할 때, DI(의존성 주입)을 도와주는 어노테이션.
스프링 컨테이너에 빈을 모두 등록한 후, 의존성 주입 단계가 이루어짐.
이 때 @Autowired 어노테이션이 부여된 메서드가 실행되며 필요한 인스턴스를 주입해준다.
생성자, 필드, setter -> 이 3가지 방법으로 실현 가능
필드로 주입하는 방식은 단점이 많아 테스트 코드에서 한정적으로 사용.

@Autowired는 내가 사용한 필드 방식만 간단하게 알아보기로 하고 다음에 필요시 더 깊게 해보기로 한다 !

 

또한 @MockBean으로 테스트할 컨트롤러 클래스에서 주입받고 있는 객체에 대해 가짜 객체를 생성해 사용한다.

 


서비스 테스트 에서는 테스트 클래스 제일 상단에 @ExtendWith(MockitoExtension.class)를 붙여준다.

이는 @Mock 어노테이션을 사용하기 위함이다.

 

@InjectMocks를 이용해 @Mock으로 만들어준 객체들을 주입받는 == 테스트 할 서비스 클래스 를 선언한다.

 

기본 세팅

@ExtendWith(MockitoExtension.class)
class FriendServiceImplTest {

    @Mock
    private MemberRepository memberRepository;

    @Mock
    private FriendRepository friendRepository;

    @Mock
    private FundingProductRepository fundingProductRepository;

    @InjectMocks
    private FriendService friendService;
}

 

친구 요청 성공 테스트 완료

@ExtendWith(MockitoExtension.class)
class FriendServiceImplTest {

    @Mock
    private MemberRepository memberRepository;

    @Mock
    private FriendRepository friendRepository;

    @Mock
    private FundingProductRepository fundingProductRepository;

    @InjectMocks
    private FriendServiceImpl friendService;

    private MemberAdapter memberAdapter1() {
        return new MemberAdapter("soni@naver.com", "qwerty");
    }

    private MemberAdapter memberAdapter2() {
        return new MemberAdapter("buni@naver.com", "qwerty");
    }

    private Member member1() {
        return Member.builder()
                .id(1L)
                .name("소은")
                .phone("010-1111-1111")
                .email("soni@naver.com")
                .password("qwerty")
                .address("서울특별시 강남구")
                .birthDay(LocalDate.of(2000, 01, 28))
                .build();
    }

    private Member member2() {
        return Member.builder()
                .id(2L)
                .name("버니")
                .phone("010-2222-2222")
                .email("buni@naver.com")
                .password("qwerty")
                .address("서울특별시 강남구")
                .birthDay(LocalDate.of(2000, 01, 28))
                .build();
    }

    @Test
    @DisplayName("친구 요청 성공 테스트")
    void friendRequestSuccessTest() {
        //given
        FriendRequest.Request request =
                new FriendRequest.Request("buni@naver.com");

        //요청 보내는 사용자 member1 받는 사용자 member2
        given(memberRepository.findByEmail(memberAdapter1().getUsername()))
                .willReturn(Optional.of(member1()));

        given(memberRepository.findByEmail(request.getEmail()))
                .willReturn(Optional.of(member2()));

        ArgumentCaptor<Friend> captor =
                ArgumentCaptor.forClass(Friend.class);

        //when
        FriendRequest.Response response =
                friendService.request(request, memberAdapter1());

        //then
        verify(memberRepository, times(2)).findByEmail(anyString());
        verify(friendRepository, times(1)).save(captor.capture());
        assertEquals(member1().getEmail(), captor.getValue().getMemberRequest().getEmail());
        assertEquals(member2().getEmail(), captor.getValue().getMember().getEmail());
        assertEquals(FriendState.WAIT, captor.getValue().getFriendState());
    }

이제 좀 테스트 코드가 눈에 익는 것 같다.

내가 어떤 메소드를, 무엇을 테스트하고 싶은지를 계속 생각하면서 점검하자 !

 

친구 요청 실패 테스트 - 요청 건 사용자를 찾을 수 없음

@Test
    @DisplayName("친구 요청 실패 테스트 - 요청 건 사용자를 찾을 수 없음")
    void friendRequestFailTest_UserNotFound() {
        //given
        FriendRequest.Request request =
                new FriendRequest.Request("buni@naver.com");

        //요청 보내는 사용자 member1 받는 사용자 member2
        given(memberRepository.findByEmail(memberAdapter1().getUsername()))
                .willReturn(Optional.empty());

        given(memberRepository.findByEmail(request.getEmail()))
                .willReturn(Optional.of(member2()));

        //when
        FriendException friendException = assertThrows(
                FriendException.class,
                () -> friendService.request(request, memberAdapter1()));

        //then
        assertEquals(ErrorType.USER_NOT_FOUND, friendException.getErrorCode());
    }

이 테스트 코드를 돌렸더니 실패가 뜨면서 stubbing 오류가 났다.

 

전에 한 번 봤었는데 불필요한 것들을 mocking 했을 때 발생하는 오류라고 했었다.

 

메소드의 제일 첫 부분인 요청 건 사용자를 찾는 과정에서 이미 Optional.empty()로 오류를 띄워줄 것이기 때문에 아래 요청 대상 사용자의 mocking 부분은 필요가 없는 것이다. 제거해주니까 테스트 성공 !

 

 

친구요청 실패 테스트 = 이미 요청 건 상대에게 요청을 걸 수 없음

private void validateRequest(Member sendMember, Member receiveMember) {
        if (Objects.equals(sendMember.getId(), receiveMember.getId())) {
            throw new FriendException(NOT_ALLOWED_YOURSELF);
        }

        Optional<Friend> sendFriendOptional =
            friendRepository.findByMemberRequestAndMember(
                sendMember, receiveMember
            );

        sendFriendOptional.ifPresent(friend -> {
            if (friend.getFriendState() == WAIT) {
                throw new FriendException(ALREADY_SEND_REQUEST);
            } else if (friend.getFriendState() == ACCEPT) {
                throw new FriendException(ALREADY_FRIEND_MEMBER);
            }
        });

        Optional<Friend> receiveFriendOptional =
            friendRepository.findByMemberRequestAndMember(
                receiveMember, sendMember
            );

        receiveFriendOptional.ifPresent(friend -> {
            throw new FriendException(ALREADY_RECEIVE_FRIEND_REQUEST);
        });
    }

 

request 메소드 중에서 validate로 빼놓은 메소드에 관한 검증 중 아래와 같은 오류가 발생

org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <com.soeun.GiftFunding.exception.FriendException> but was: <org.mockito.exceptions.misusing.PotentialStubbingProblem>

	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:65)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:37)
	at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3082)
	at com.soeun.GiftFunding.service.FriendServiceImplTest.friendRequestFailTest_AlreadySendRequest(FriendServiceImplTest.java:199)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.util.ArrayList.forEach(ArrayList.java:1259)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.util.ArrayList.forEach(ArrayList.java:1259)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: org.mockito.exceptions.misusing.PotentialStubbingProblem: 
Strict stubbing argument mismatch. Please check:
 - this invocation of 'findByMemberRequestAndMember' method:
    friendRepository.findByMemberRequestAndMember(
    com.soeun.GiftFunding.entity.Member@590c73d3,
    com.soeun.GiftFunding.entity.Member@6b9ce1bf
);
    -> at com.soeun.GiftFunding.service.FriendServiceImpl.validateRequest(FriendServiceImpl.java:70)
 - has following stubbing(s) with different arguments:
    1. friendRepository.findByMemberRequestAndMember(
    com.soeun.GiftFunding.entity.Member@6f2cb653,
    com.soeun.GiftFunding.entity.Member@14c01636
);
      -> at com.soeun.GiftFunding.service.FriendServiceImplTest.friendRequestFailTest_AlreadySendRequest(FriendServiceImplTest.java:190)
Typically, stubbing argument mismatch indicates user mistake when writing tests.
Mockito fails early so that you can debug potential problem easily.
However, there are legit scenarios when this exception generates false negative signal:
  - stubbing the same method multiple times using 'given().will()' or 'when().then()' API
    Please use 'will().given()' or 'doReturn().when()' API for stubbing.
  - stubbed method is intentionally invoked with different arguments by code under test
    Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).
For more information see javadoc for PotentialStubbingProblem class.
	at com.soeun.GiftFunding.service.FriendServiceImpl.validateRequest(FriendServiceImpl.java:70)
	at com.soeun.GiftFunding.service.FriendServiceImpl.request(FriendServiceImpl.java:50)
	at com.soeun.GiftFunding.service.FriendServiceImplTest.lambda$friendRequestFailTest_AlreadySendRequest$3(FriendServiceImplTest.java:200)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:55)
	... 72 more

Mock 객체로 생성한 member 들과 실제 전달 된 member 들이 다르다는 말이다.

 

왜 그런가 생각해봤더니 나는 코드의 중복을 해소하기 위해 private 메소드로 member1(), member2()를 만들어서 썼다.

이 두 개 메소드의 리턴값은 new로 Member 객체를 생성해서 반환한다. 그래서 매번 호출 할 때마다 member 객체가 달라지는 것 !

 

더 정확히는 각 로그인한 사용자(member1), 친구 요청 대상 사용자(member2)가 mocking으로 각각 member1(), member2() 메소드를 통해 나온 Member 객체인데 3번째 given절의 findByMemberRequesetAndMember에 전해지는 파라미터로 member1(), member2()를 하면 위의 mocking 객체와 다른 객체가 되어버려서 오류가 나는 것 !

 

아래는 내 생각을 확신하려고 1,2번째 given절은 메소드로 호출, 3번째 given절은 메소드내에 만들어둔 member 객체들을 사용했다. 역시나 똑같은 오류가 발생하고 given절의 willReturn값도 메소드 내 필드 member1, 2로 바꿔주면 테스트가 통과한다 ! 

해결 :

member1(), member2()의 반환 값을 해당 테스트 메소드의 파라미터로 만들어두고 똑같은 객체를 가지고 테스트 할 수 있게 하였다.

 

@Test
    @DisplayName("친구 요청 실패 테스트 - 이미 요청 건 상대에게 요청 걸 수 없음")
    void friendRequestFailTest_AlreadySendRequest() {
        //given
        FriendRequest.Request request =
                new FriendRequest.Request("buni@naver.com");

        Member member1 = member1();
        Member member2 = member2();
        //요청 보내는 사용자 member1 받는 사용자 member2
        given(memberRepository.findByEmail(memberAdapter1().getUsername()))
                .willReturn(Optional.of(member1));

        given(memberRepository.findByEmail(request.getEmail()))
                .willReturn(Optional.of(member2));

        given(friendRepository.findByMemberRequestAndMember(member1, member2))
                .willReturn(Optional.of(Friend.builder()
                        .id(1L)
                        .member(member2)
                        .memberRequest(member1)
                        .friendState(FriendState.WAIT)
                        .build()));

        //when
        FriendException exception = assertThrows(FriendException.class,
                () -> friendService.request(request, memberAdapter1()));

        //then
        assertEquals(ErrorType.ALREADY_SEND_REQUEST, exception.getErrorCode());
    }

 


ArgumentCaptor

그냥 무의식 적으로 썼던 ArgumentCaptor를 제대로 정리해보려고 한다.

 

- ArgumentCaptor란? ) 의존하고 있는 Mock에 전달된 데이터가 내가 의도하고 있는 데이터가 맞는지 확인.

   => 메소드에 들어가는 인자 값 검증

 

나는 친구 목록 조회 성공 테스트에서

findByEmail의 인자로 들어가는 값을 captor.capture()로 저장을 해서 내가 보내준 memberAdapter의 값이 맞는지 검증하고자 하였다.

 

@Test
    @DisplayName("친구 목록 조회 성공 테스트 - WAIT")
    void friendListSuccessTest_WAIT() {
        //given
        Member member1 = member1();
        Member member2 = member2();
        Member member3 = member3();

        Pageable pageable = PageRequest.of(0, 10);

        given(memberRepository.findByEmail(memberAdapter1().getUsername()))
                .willReturn(Optional.of(member1));

        List<Friend> friendList =
                Arrays.asList(
                        Friend.builder()
                                .id(1L)
                                .member(member1)
                                .memberRequest(member2)
                                .friendState(FriendState.WAIT)
                                .build(),
                        Friend.builder()
                                .id(1L)
                                .member(member1)
                                .memberRequest(member3)
                                .friendState(FriendState.WAIT)
                                .build());

        given(friendRepository.findByMemberAndFriendState(
                member1, FriendState.WAIT, pageable))
                .willReturn(new PageImpl(friendList));

        ArgumentCaptor<String> captor =
                ArgumentCaptor.forClass(String.class);

        //when
        Page<FriendList> resultFriendList = friendService.friendList(
                memberAdapter1(), FriendState.WAIT, pageable);

        //then
        assertEquals(2, resultFriendList.getSize());
        verify(memberRepository, times(1)).findByEmail(captor.capture());
        assertEquals(memberAdapter1().getUsername(), captor.getValue());
        assertEquals("buni@naver.com", resultFriendList.getContent().get(0).getMemberEmail());
        assertEquals("bucks@naver.com", resultFriendList.getContent().get(1).getMemberEmail());
        verify(friendRepository, times(1)).findByMemberAndFriendState(member1, FriendState.WAIT, pageable);
        verify(friendRepository, times(0)).save(any());
    }

//then의 두 번째 줄 verify에서 findByEmail로 들어가는 인자 값을 captor.capture()로 저장하고 

assertEquals로 memberAdapter의 값이 맞는지 검증하였다. !

 


Mocking을 어디까지 해줘야 하는가 ?

친구의 펀딩상품 조회 메소드를 테스트 하면서 이 부분은 에러가 터질 걸 알면서 그냥 냅두고 해봤다.

@Override
    public FriendFundingProduct friendProduct(
            MemberAdapter memberAdapter, Long id, Pageable pageable) {

        Member member = memberRepository.findByEmail(memberAdapter.getUsername())
            .orElseThrow(() -> new FriendException(ErrorType.USER_NOT_FOUND));

        Member friend = memberRepository.findById(id)
            .orElseThrow(() -> new FriendException(USER_NOT_FOUND));

        if (!friendRepository.existsByFriendStateAndMemberRequestAndMember(
            ACCEPT, member, friend)) {
            throw new FriendException(FRIEND_INFO_NOT_FOUND);
        }
        return FriendFundingProduct.builder()
            .name(friend.getName())
            .phone(friend.getPhone())
            .email(friend.getEmail())
            .birthDay(friend.getBirthDay())
            .fundingProductList(fundingProductRepository.findByMemberAndFundingState(
                    friend, ONGOING, pageable)
                .map(FundingProduct::toDto))
            .build();
    }

위의 메소드를 테스트 하는 아래 코드에서 NullPointerException 에러가 난다

 

@Test
    @DisplayName("친구의 펀딩상품 조회 성공 테스트")
    void friendProductSuccessTest() {
        //given
        Pageable pageable = PageRequest.of(0, 10);
        Long friendId = 1L;

        Member member = member1();
        Member friend = member2();

        given(memberRepository.findByEmail(memberAdapter1().getUsername()))
                .willReturn(Optional.of(member));

        given(memberRepository.findById(friendId))
                .willReturn(Optional.of(friend));

        given(friendRepository.existsByFriendStateAndMemberRequestAndMember(
                FriendState.ACCEPT, member, friend))
                .willReturn(true);

        //when
        FriendFundingProduct response =
                friendService.friendProduct(
                        memberAdapter1(), friendId, pageable);

        //then
        assertEquals(member2().getEmail(), response.getEmail());
    }

mocking을 어디까지 해줬냐면 로그인한 사용자를 검증하는 findByEmail,

친구의 펀딩상품을 조회하기 위해 친구 아이디로 사용자를 검증하는 findById,

친구인 상태의 정보가 있는지를 조회하는 existsByFriendStateAndMemberRequestAndMember를 해주었다.

 

여기서 NPE가 터진 이유는 리턴 값에서 fundingProductRepository.findByMemberAndFundingState를 Mocking 안 해줬기 때문 !

 

다른 코드들 어디에서도 fundingProductRepository를 해준 적 없지만 오류가 터지지 않은 이유는 당연하게도 메소드 내에서 사용하지 않았기 때문이고

 

FriendServiceImpl에서는 

private final MemberRepository memberRepository;
private final FriendRepository friendRepository;
private final FundingProductRepository fundingProductRepository;

이렇게 3개의 repository 의존성을 주입받고 있다.

 

따라서 테스트 코드에서 @Mock으로 가짜로 만들어 사용할 객체임을 알려주었고 각 메소드에서 실제 쓰이는 부분엔 given절을 이용해 필수적으로 Mocking을 해줘야 NPE가 나지 않는다 !

 

해결

@Test
    @DisplayName("친구의 펀딩상품 조회 성공 테스트")
    void friendProductSuccessTest() {
        //given
        Pageable pageable = PageRequest.of(0, 10);
        Long friendId = 1L;

        Member member = member1();
        Member friend = member2();

        given(memberRepository.findByEmail(memberAdapter1().getUsername()))
                .willReturn(Optional.of(member));

        given(memberRepository.findById(friendId))
                .willReturn(Optional.of(friend));

        given(friendRepository.existsByFriendStateAndMemberRequestAndMember(
                FriendState.ACCEPT, member, friend))
                .willReturn(true);

        List<FundingProduct> fundingProductList =
                Arrays.asList(
                        FundingProduct.builder()
                                .id(1L)
                                .product(new Product(1L, "반지", 10000L, 1))
                                .member(friend)
                                .total(5000L)
                                .fundingState(FundingState.ONGOING)
                                .build(),
                        FundingProduct.builder()
                                .id(2L)
                                .product(new Product(2L, "목걸이", 20000L, 2))
                                .member(friend)
                                .total(1000L)
                                .fundingState(FundingState.ONGOING)
                                .build()
                );

        given(fundingProductRepository.findByMemberAndFundingState(
                friend, FundingState.ONGOING, pageable))
                .willReturn(new PageImpl(fundingProductList));

        //when
        FriendFundingProduct response =
                friendService.friendProduct(
                        memberAdapter1(), friendId, pageable);

        //then
        assertEquals(member2().getEmail(), response.getEmail());
    }

더 세세하게 테스트를 해줘야 하지만 일단 이렇게 fundingProductRepository를 Mocking을 해주면 NPE가 해결된다 !


이번 서비스 클래스 테스트를 통해 알게된 점

1. @ExtendWith(MockitoExtension.class) 달아줘야 @Mock 사용가능 @Mock으로 의존하는 가짜 객체들을 만들어주고 @InjectMocks로 테스트 할 서비스 클래스에 주입

2. 나는 인터페이스로 XxxService를 만들고 XxxServiceImpl로 구현을 해서 사용하는 구조를 적용했는데
@InjectMocks에는 XxxService가 아닌 XxxServiceImpl로 필드를 선언해줘야한다, 인터페이스에는 주입할 수 없다고 함 !

3. ArgumentCaptor<테스트하고픈인자> : ArgumentCaptor는 메소드의 인자로 넘어가는 값이 내가 원하는 값인지를 테스트 할 수 있다 !

4. 에러를 던지는 부분을 테스트하고 싶다면
.given(~~).willThrow(MemberException.class)나
.given(~~).willThrow(new MemberException(ErrorType.USER_NOT_FOUND)를 사용할 수 있다.

5. private 메소드를 테스트하는 것은 지양한다. 어떤 것을 테스트하고 싶은지 생각하고 케이스를 나누어 테스트하는 것이 올바른 방법이다.