본문 바로가기
프로젝트/USports

[USports] Member 단위 테스트

by JayAlex07 2023. 12. 7.

[USports] Member 단위 테스트

 

Mock

  • 서비스를 테스트 할 때에 필요한 외부 클래스들을 넣었다
  • repository, passwordEncoder 등 MemberServiceImpl에 넣었던 클래스들을 Mock으로 넘겼다

 

InjectMocks

  • Mock으로 설정했던 클래스들을 MemberServiceImpl에 넣는 것이다

 

Nested

  • 테스트들을 단위별로 묶을 수 있다
  • 예를 들어, 로그인 테스트, 회원가입 테스트 별로 나누고, 그 안에 성공 케이스와 실패 케이스를 넣을 수 있다

 

when, thenReturn

  • 주로 메서드 내에 repository를 사용하던지, 또는 Mock으로 설정된 클래스를 사용할 때에 when을 사용한다.
  • 그리고 thenReturn을 통해서 when에서 불러온 메서드를 통해 어떤 결과값을 나오게 할지, 적어준다!
    • 이렇게 thenReturn에서 리턴 값을 어떻게 설정하느냐에 따라서 성공 테스트 그리고 실패 테스트를 만든다
  • 삭제 코드 같은 경우 verfiy(memberRepository, times(1)).deleteById(memberId) 를 사용한다 (테스트 할 매서드 호출 후, assertThat 위치에)
  • 그럼 왜 이렇게 하는 것일까?
    • 계속 고민 했던 부분이다. 하지만, 생각해보면, 테스트 코드, 특히 단위 테스트에서는 DB에 따로 저장 또는 삭제할 수 없다.
    • 그래서 when/thenReturn 등을 통해서 상상의 DB에 넣거나, 또는 DB에서 어떤 값을 가져오거나, 할 수 있다고 생각했다!

 

TestCode 작성하면서 느낀 것

  • 성공 테스트는 메서드 코드를 하나하나 읽어가며, 어떤 부분을 when에 넣을지 생각을 한다
  • 그리고 성공 테스트가 끝났으면, 그 코드를 복붙한다
    • 실패 테스트는 메서드 코드를 뒤에서부터 읽으면서, 에러가 던져지는 코드를 발견하면, 그때 첫 실패 테스트를 한다
  • 즉 성공은 처음부터 리턴할 때까지, 실패는 리턴 지점에서 메서드 코드 시작하는 지점까지 한 바퀴를 돌며 테스트를 진행하는 것이 더 쉬웠다
    • 특히 실패 테스트 할 때

 

@ExtendWith(MockitoExtension.class)
@Slf4j
public class MemberServiceTest {

    @Mock
    private MemberRepository memberRepository;

    @Mock
    private PasswordEncoder passwordEncoder;

    @InjectMocks
    private MemberServiceImpl memberService;

    @Builder
    private static MemberEntity member(Long memberId, String accountName, String name, String email,
                               String password, String phoneNumber, LocalDate birthDate,
                               Gender gender, String profileContent, String profileImage,
                               LocalDateTime emailAuthAt, String addrCity, String addrDistrict,
                               boolean profileOpen, Role role) {
        return MemberEntity.builder()
                .memberId(memberId)
                .accountName(accountName)
                .name(name)
                .email(email)
                .password(password)
                .phoneNumber(phoneNumber)
                .birthDate(birthDate)
                .gender(gender)
                .profileContent(profileContent)
                .profileImage(profileImage)
                .emailAuthAt(emailAuthAt)
                .addrCity(addrCity)
                .addrDistrict(addrDistrict)
                .profileOpen(profileOpen)
                .role(role)
                .build();
    }

    @Nested
    @DisplayName("첫 회원가입")
    class RegisterMemberTest {

        @Test
        @DisplayName("회원가입 성공")
        void successFirstRegisterMember() {
            //given
            LocalDate birthDate = LocalDate.parse("1996-02-17", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
            String password = passwordEncoder.encode("abcd1234!");

            MemberEntity member = member(1L, "joons", "Je Joon", "joons@gmail.com", password,
                    "010-1234-1234", birthDate, Gender.MALE, null, null, null, null, null,
                    true, Role.UNAUTH);

            MemberRegister.Request request = MemberRegister.Request.builder()
                    .accountName("joons")
                    .name("Je Joon")
                    .email("joons@gmail.com")
                    .password(password)
                    .phoneNumber("010-1234-1234")
                    .birthDate(birthDate)
                    .gender(Gender.MALE)
                    .profileOpen("open")
                    .build();

            //when
            when(memberRepository.save(MemberRegister.Request.toEntity(request)))
                    .thenReturn(member);

            MemberRegister.Response response = memberService.registerMember(request);

            //then
            assertThat(response.getAccountName()).isEqualTo(member.getAccountName());
            assertThat(response.getEmail()).isEqualTo(member.getEmail());
            assertThat(response.isProfileOpen()).isEqualTo(member.isProfileOpen());
            assertThat(response.getMessage()).isEqualTo(MailConstant.AUTH_EMAIL_SEND);
        }

        @Test
        @DisplayName("회원가입 실패 - 닉네임이 이미 있음")
        void failAccountAlreadyExist() {
            //given
            LocalDate birthDate = LocalDate.parse("1996-02-17", DateTimeFormatter.ofPattern("yyyy-MM-dd"));

            MemberRegister.Request request = MemberRegister.Request.builder()
                    .accountName("joons")
                    .name("Je Joon")
                    .email("joons@gmail.com")
                    .password("Aabcd1234!")
                    .phoneNumber("010-1234-1234")
                    .birthDate(birthDate)
                    .gender(Gender.MALE)
                    .profileOpen("open")
                    .build();

            //when
            when(memberRepository.existsByAccountName(request.getAccountName()))
                    .thenReturn(true);

            MemberException exception =
                    catchThrowableOfType(() ->
                            memberService.registerMember(request), MemberException.class);

            //then
            assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.ACCOUNT_ALREADY_EXISTS);
        }
    }
}

 

Stubbing

 

계속 하면서 제일 골치 아팠던 에러가 위의 에러다

  • 이는 객체가 생성 될 때마다, 다른 객체가 생성되서이기 때문이다
    • 객체가 생성 될 때마다 HashCode가 주어진다
    • 하지만, 테스트를 하면서 여러 객체가 생성되어도, 같은 객체가 생성되길 원할 때가 있다
      • 예를 들어, A라는 객체를 만들고, A에 대한 객체가 A인지 확인 할 때에
      • A라는 객체를 확인하기 위해 필드가 모두 같은 객체를 만들었지만 B가 만들어져서 에러가 발생

 

any(MemberEntity.class)

  • any 같은 경우, 정말 아무 데이터를 넣는 것이라서, 정확한 테스트를 위해서는
    • when(memberRepository.save(any(MemberEntity.class))).thenReturn(memberEntity);
    • 위 보다 when(memberRepository.save(memberEntity).thenReturn(memberEntity);를 넣는 것이 좋다
    • 하지만 아래와 같이 넣으면 Stubbing 에러가 발생한다

 

@EqualsAndHashCode

  • 각 엔티티에 어노테이션을 붙이는 것이다
  • 이 어노테이션을 통해, 자동으로 만들어지는 HashCode가 아닌, 원하는 필드를 기반으로 HashCode를 만들고, 비교하는 매서드를 오버라이드 할 수 있다
  • 하지만 어노테이션 자체에서 문제가 생길 수 있다

 

직접 오버라이드 하기

  • alt + ins 을 누르면 EqualsAndHashCode를 볼 수 있다
  • 거기서 원하는 필드를 선택하면 알아서 오버라이드를 해준다
  • memberId로 Hashing을 하고, 비교를 할 때에 MemberId를 토대로 비교를 해준다
@Entity(name = "Member")
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Builder
@EntityListeners(AuditingEntityListener.class)
public class MemberEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id", nullable = false)
    private Long memberId;

    @Column(name = "account_name", nullable = false, unique = true, length = 100)
    private String accountName;

    @Column(name = "email", nullable = false, unique = true, length = 100)
    private String email;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "profile_open", nullable = false)
    private boolean profileOpen;

    @Column(name = "role", nullable = false)
    @Enumerated(EnumType.STRING)
    private Role role;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MemberEntity that = (MemberEntity) o;
        return Objects.equals(memberId, that.memberId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(memberId);
    }
}

'프로젝트 > USports' 카테고리의 다른 글

[USports] Websocket 소개  (0) 2023.12.18
[USports] Member OAuth2  (0) 2023.12.09
[USports] Email 전송  (1) 2023.12.06
[USports] Login 구현  (1) 2023.12.05
[USports] Redis + Login  (9) 2023.12.05