Dev_Henry

[Spring] mock을 이용한 mvc 단위테스트 - 2. service 본문

Web/Spring

[Spring] mock을 이용한 mvc 단위테스트 - 2. service

데브헨리 2023. 7. 25. 00:10
728x90

 

1편에 이어서 service의 테스트 코드를 만들어 보자.

 

  • memberServiceImpl
@slf4j
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{
    private final AccountUtil accountUtil;
    private final ModelMapper modelMapper;
    private final MemberRepository memberRepository;
    private final PostRepository postRepository;
    private final ScrapRepository scrapRepository;
    private final PasswordEncoder passwordEncoder;
    @Transactional
    @Override
    public void join(MemberJoinDTO memberJoinDTO) {
        String memberId = memberJoinDTO.getMemberId();
        if(memberRepository.existsById(memberId)){
            throw new MemberIdExistException();
        }
        if(memberRepository.existsByNickName(memberJoinDTO.getNickName())){
            throw new MemberIdExistException(NICKNAME_ALREADY_EXIST);
        }
        Member member = modelMapper.map(memberJoinDTO,Member.class);
        member.updatePassword(passwordEncoder.encode(memberJoinDTO.getMemberPassword()));
        member.addRole(MemberRole.USER);

        memberRepository.save(member);
    }

.
.
.
}
 
 

 

(아래는 mockito에 대해 잘 모르는 상태로 테스트 코드를 작성하며 실패과정이 포함 되어있으며 정상 작동하는 최종코드는 가장 밑에 있습니다.)

 

- 1차 시도(실패)

@ExtendWith(MockitoExtension.class)
class MemberServiceTests {
    @InjectMocks
    private MemberService memberService;
    @Mock
    private MemberRepository memberRepository;
    
    @DisplayName("회원가입")
    @Test
    void joinTest() {
        // given
        String name = "jointest999";
        MemberJoinDTO memberJoinDTO = joinDTO(name);

        //when
        memberService.join(memberJoinDTO);
        Member member = memberRepository.findByMemberId(name).orElseThrow();

        // then
        Assertions.assertThat(member.getMemberId()).isEqualTo(name);
    }

    private MemberJoinDTO joinDTO(String name) {
        return MemberJoinDTO.builder()
                .memberId(name).memberPassword("a1234567").name("테스트")
                .nickName(name).birthDay(LocalDate.now())
                .phone("01012345678").height(180).weight(70).gender("M")
                .build();
    }
}
 
  • @ExtendWith(MockitoExtension.class) : JUnit4에서는 @RunWith를 사용했지만 JUnit5부터는 해당 어노테이션으로 mockito를 사용한다는 것을 명시해준다.
  • @InjectMocks : 테스트 할 수 있도록 mock객체를 주입시켜준다.
  • @Mock : 관심사 밖의 내용에 대해 가짜 객체를 만들어준다.

 

해당 테스트코드는 작동하지 않으며 우선 찾은 원인은 두가지였다.

  1. 현재 프로젝트에서 memberService는 인터페이스로 두고 memberServiceImpl을 사용하기 때문에 테스트를 할때도 구현클래스를 사용해야한다.(원래는 스프링이 자동으로 주입해줬지만 지금은 단위 테스트라 따로 주입해줘야함)
  2. 모키토에 대해 잘 몰라서 구글링에서 나온 예제만 보고 따라했던 건데 memberService를 돌리기 위해 필요한 녀석들을 mock객체로 주입해주는 것이기 때문에 memberRepository뿐만 아니라 service에서 필요한 클래스들을 모두 mock선언해주어야한다.

 

 

- 2차 시도(실패)

@ExtendWith(MockitoExtension.class)
class MemberServiceTests {
    @InjectMocks
    private MemberServiceImpl memberService;
    @Mock
    private MemberRepository memberRepository;
    @Mock
    private AccountUtil accountUtil;
    @Mock
    private ModelMapper modelMapper;
    @Mock
    private PostRepository postRepository;
    @Mock
    private ScrapRepository scrapRepository;
    @Mock
    private PasswordEncoder passwordEncoder;
    
.
.
.
}
 

 

null에러가 떴고 중단점을 찍어 디버깅을 해보니

modelMapper가 제대로 작동하지 않았다.

 

원인을 찾아보니 Mock으로 만들어진 modelMapper가 완전한 껍데기 객체이기 때문에 제대로 역할을 할 수 없었다.

이를 방지하기 위해 원본 메서드를 그대로 사용할 수 있도록 하는 @Spy 어노테이션을 @Mock대신 붙어주어야 한다.

+ 또는 mock객체로 선언후에 modelMapper.map()의 동작을 직접 설정해주는 방법도 있다. (when(modelMapper.map(memberJoinDTO, Member.class)).thenReturn(member);)

 

- 3차 시도 (실패 - 테스트코드와는 관계없는 이유)

.
.
    @Spy
    private ModelMapper modelMapper;
    @Spy
    private BCryptPasswordEncoder passwordEncoder;
.
.
 

이번에는 값을 찾을수가 없어 다시 디버깅을 해보았다.

이번에는 modelmapper는 제대로된 객체가 있고, member도 만들어 주었는데 값이 바인딩이 안되어있다..

 

 

- 4차 시도 (실패)

 

계속해서 찾아보다가 Member Entity에 setter가 없는 것을 발견했다.

그래서 혹시 하고 찾아보니 modelmapper에는 반드시 setter가 필요했고, member에 @setter를 달아주니 정상적으로 Member에 값이 바인딩 되었다. (테스트환경이 아닌 정상적인 실행에서 이제까지 setter없이도 잘 동작했는데 왜 그런지는 아직 모르겠다..다음에 찾아보자)

    @DisplayName("회원가입")
    @Test
    void joinTest() {
        // given
        String name = "jointest999";
        MemberJoinDTO memberJoinDTO = joinDTO(name);

        //when
        memberService.join(memberJoinDTO);
        Member member = memberRepository.findByMemberId(name).orElseThrow();

        // then
        Assertions.assertThat(member.getMemberId()).isEqualTo(name);
    }
 

하지만 이번엔 다른 에러가 발생했다.

회원가입을 시킨 뒤 가입된 아이디를 가져와서 값을 비교하는 방식으로 테스트를 작성했는데, 가입된 아이디를 가져오는 부분에서 예외로 던져진다.

고민해본 결과 처음에 의심했던 부분은

첫번째는 memberService.join에서 정상적으로 member를 만들어서 save시키지만 @Transactional에 의해 커밋되지 않고 롤백이 되는 경우,

두번째는 memberRepository가 mock객체이기 때문에 정상적인 작동을 하지 않는 경우

이렇게 두가지 경우였다.

그래서 modelMapper처럼 repository에 @mock대신 @spy도 줘보았지만 실패했다.

아마 repository를 @spy로 만들더라도, 단위테스트이기 때문에 repository가 객체를 저장하는 위치(db설정) 관련해서 문제가 생기는 듯 했다.

 

 

- 최종 5차 시도 (성공)

 

계속 구글링을 해보며 테스트코드 작성에 대해 공부하다보니,

verify 를 사용해 단순히 해당 메소드를 호출했는지 검사하는 방법으로 테스트하는 사람들도 많았다.

단순 호출만 검사하는건 내키지 않아서 코드를 조금 수정했다.

 

우선 service의 join메서드가 저장된 member를 리턴하도록 바꾸고 mock객체인 repository의 동작도 직접 설정해줬다.

(사실 테스트를 위해서 굳이 필요없는 리턴값을 만들어주는게 좋은 방법인지는 잘 모르겠다. 뭔가 단순하게 호출 했는지보다 결과로 나온 값을 직접 비교해보는게 더 정확할 것같은 느낌..)

 

+ 나중에 다시 생각해보니 어차피 repositoty의 동작은 따로 테스트하고, when(memberRepository.save(any(Member.class))).thenReturn(member); 처럼 결과로 나올 값을 직접 설정해줬기때문에 save가 잘 호출되는지만 봐도 상관없겠다..

 

 

  • memberServiceImpl
    @Transactional
    @Override
    public Member join(MemberJoinDTO memberJoinDTO) {

...

        return memberRepository.save(member);
    }
 

 

  • memberServiceTests
    @DisplayName("회원가입")
    @Test
    void joinTest() {
        // given
        String name = "jointest999";
        MemberJoinDTO memberJoinDTO = joinDTO(name);
        Member member = joinResponse(name);
        when(memberRepository.save(any(Member.class))).thenReturn(member);

        //when
        Member result = memberService.join(memberJoinDTO);

        // then
        Assertions.assertThat(result).isEqualTo(member);
    }
.
.
    private Member joinResponse(String name){
        return Member.builder()
                .memberId(name).memberPassword("a1234567").gender('M').nickName(name)
                .birthDay(LocalDate.now()).del(false).height(180L).weight(70L).name("테스트")
                .phone("01012345678").profilePhoto(null).social(false).roleSet(Collections.singleton(MemberRole.USER))
                .build();
    }
}
 
  • when().thenReturn() : service내부의 repository는 mock객체이기 때문에 기대하는 정상적인 작동을 하지 못한다. 그렇기 때문에 해당 코드를 이용하여 repository.save가 실행되면 정해둔 member객체를 리턴하도록 미리 설정해두는 것이다.

 

결과

 

728x90
반응형