티스토리 뷰

문제상황

‘TWTW’의 테스트는 크게 Controller, Service, Repository Layer에서 진행되었다. 우리의 목표는 단위 테스트 적용이었다. 하지만 Service 테스트 코드 내에서 Repository를 통한 실제 DB 접근이 이루어져 완벽한 단위 테스트를 수행할 수 없었다.

 

Repository Layer의 Mock 처리?

  • Mock으로 처리할 수도 있다. 하지만 단위테스트 도중 무분별한 중복 Mock 사용이 많아졌다.
  • 또한, Mock으로는 동적인 테스트 불가로 구조 개선을 고민했다.
    • Ex) 하나의 테스트 내에서 save한 엔티티를 조회해야 한다면 두 번의 Mock을 거쳐야 하고 동적으로 테스트가 불가하게 되며, Mock으로 시작해 Mock으로 끝나는 테스트가 되어버린다.

접근 방식

  • Repository Test
    • DB를 통한 접근이 수행되는가에 초점을 맞추어 테스트 코드 작성
  • Service Test
    • Fake를 활용하여 DB 접근을 하지 않고 서비스 로직에만 초점을 맞추어 테스트 코드 작성
  • Controller Test
    • mock을 활용하여 Service 로직을 타지 않고 테스트 수행
    • 테스트를 수행하면서 자동으로 rest docs 생성

테스트용 Repository 분리

  • 공통 인터페이스인 MemberRepository를 만들었다.
  • 기존에 사용 중인 JpaMemberRepository와 새로 구현할 FakeMemberRepository가 이를 상속받는다.
  • Service Layer는 공통 인터페이스인 MemberRepository를 사용한다.
  • FakeMemberRepository는 DB PK를 통한 조회가 많음을 고려해 Map<PK, Entity>를 가져 엔티티를 저장하며 jpa와 동일하게 동작할 수 있게 구현했다.
    • @TestConfiguration 이 붙은 테스트 설정 클래스에 @Primary와 함께 MemberRepository 타입으로 FakeMemberRepository를 주입했다.

전략 패턴을 사용한 전체 구조

 

Repository의 추상화

@Repository
public interface MemberRepository {
    Optional<Member> findById(final UUID id);

    List<Member> findAllByIds(final List<UUID> friendMemberIds);

    void deleteById(final UUID memberId);
    
    /* 생략 */
}

실제 서비스용 JpaRepository

@Repository
public interface JpaMemberRepository extends JpaRepository<Member, UUID>, MemberRepository {

    @Query(
            value =
                    "SELECT * FROM member m WHERE MATCH (m.nickname) AGAINST(:nickname IN BOOLEAN"
                        + " MODE)",
            nativeQuery = true)
    List<Member> findAllByNickname(@Param("nickname") String nickname);

    /* 생략 */
}

테스트용 FakeRepository

public class FakeMemberRepository implements MemberRepository {

    private final Map<UUID, Member> map = new HashMap<>();

    @Override
    public List<Member> findAllByNickname(final String nickname) {
        return map.values().stream()
                .filter(
                        member ->
                                member.getNickname().toUpperCase().contains(nickname.toUpperCase()))
                .toList();
    }

    @Override
    public boolean existsByNickname(final String nickname) {
        return map.values().stream().anyMatch(member -> member.getNickname().equals(nickname));
    }

    @Override
    public Member save(final Member member) {
        map.put(member.getId(), member);
        return member;
    }
    
    /* 생략 */
    
}

각 기능별 FakeRepository를 만든 후 FakeConfig를 통해 테스트 시 빈으로 주입되도록 설정

@TestConfiguration
public class FakeConfig {

    private final Map<UUID, Friend> map = new HashMap<>();

    @Bean
    @Primary
    public FriendQueryRepository fakeFriendQueryRepository() {
        return new FakeFriendQueryRepository(map);
    }

    @Bean
    @Primary
    public FriendCommandRepository fakeFriendCommandRepository() {
        return new FakeFriendCommandRepository(map);
    }

    @Bean
    @Primary
    public RefreshTokenRepository refreshTokenRepository() {
        return new FakeRefreshTokenRepository();
    }
    
    /* 생략 */
}

Repository Test

Repository 테스트 시 실제 DB와의 상호작용을 테스트하도록 코드 작성

@DisplayName("MemberRepository의")
class MemberRepositoryTest extends RepositoryTest {

    @Autowired private MemberRepository memberRepository;

    @Test
    @DisplayName("soft delete가 수행되는가?")
    void softDelete() {
        // given
        final Member member = MemberEntityFixture.FIRST_MEMBER.toEntity();
        final UUID memberId = memberRepository.save(member).getId();

        // when
        memberRepository.deleteById(memberId);

        // then
        assertThat(memberRepository.findById(memberId)).isEmpty();
    }

	/* 생략 */
    
}

Service Test

서비스 로직 테스트를 위해 FakeRepository를 이용하여 테스트 작성

  • MemberServiceTest의 경우 주입받는 memberRepository는 FakeRepository
@DisplayName("MemberService의")
class MemberServiceTest extends LoginTest {
    @Autowired private MemberService memberService;
    @Autowired private MemberRepository memberRepository;

    @Test
    @DisplayName("UUID를 통해 Member 조회가 되는가")
    void getMemberById() {
        // given
        final Member member = memberRepository.save(MemberEntityFixture.FIRST_MEMBER.toEntity());

        // when
        Member response = memberService.getMemberById(member.getId());

        // then
        assertThat(response.getId()).isEqualTo(member.getId());
    }

	/* 생략 */
}

Controller Test

컨트롤러 Layer에서의 Request & Response 테스트를 위해 Service를 mock으로 만들어 테스트 작성

    @Test
    @DisplayName("닉네임이 중복되었는가")
    void duplicate() throws Exception {
        final DuplicateNicknameResponse expected = new DuplicateNicknameResponse(false);
        given(memberService.duplicateNickname(any())).willReturn(expected);

        final ResultActions perform =
                mockMvc.perform(
                        get("/member/duplicate/{name}", "JinJooOne")
                                .contentType(MediaType.APPLICATION_JSON));

        // then
        perform.andExpect(status().isOk()).andExpect(jsonPath("$.isPresent").exists());
        // docs

        perform.andDo(print())
                .andDo(
                        document(
                                "get duplicate nickname",
                                getDocumentRequest(),
                                getDocumentResponse()));
    }

얻은 효과

Fake를 사용하여 유연한 처리

  • Repository Layer가 JPA에 종속적이지 않고 테스트에 용이한 유연한 구조 가져감
  • Controller 테스트의 경우 하나의 메서드만 mocking하면 되었지만, Service 테스트에서는 많은 의존성 때문에 모두 mock으로 처리하기에 부담, 같은 메서드도 매번 mock 처리하기에도 어려움
  • 테스트와 실제 서버 배포시 서로 다른 구현체가 주입되며 원하는 방향으로 활용 가능

자잘한 트러블 슈팅

  • TestContainer 도입
    • MySQL에서 제공하는 기능을 기존에 사용하던 테스트용 H2 DB에서 지원하지 않음(FULL TEXT INDEX)
    • Redis, RabbitMQ와 같은 외부 시스템과 연동되는 부분을 원활히 테스트

테스트 기반 협업 환경 구축

  • Gradle에 Jacoco 설정을 추가하였으며, Github Actions를 통해 PR 시 Jacoco Test Coverage를 볼 수 있도록 자동화
  • Rest Docs를 통해 Controller 테스트를 수행하며 API 문서 생성

Test Coverage

  • Jacoco 도입으로 테스트시와 PR시 커버리지 확인 가능
  • Jacoco 커버리지

Rest Docs

https://hongdam-org.github.io/TWTW_Api_Docs/

 

TWTW API

Snippet http-response not found for operation::post join group

hongdam-org.github.io

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
TAG
more
«   2024/11   »
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
글 보관함