Spring boot Test 코드 탐구(1) - 테스트 종류

haaaalin·2023년 4월 16일
0

먼저 테스트를 알기 전에 좋은 테스트를 위한 5가지 원칙을 알아보자!

TEST 5가지 원칙 (FIRST)

Fast: 빠르게 수행되어야 한다

  • @SpringBootTest와 같이 통합적인 Bean을 load하는 것을 지양
  • 테스트에 필요한 Bean만 로드

Isolated: 독립적으로 수행되어야 한다

  • A테스트 결과가 B의 테스트 결과의 영향을 미쳐서는 X

Repeatable: 반복적으로 수행해도 결과가 같아야 한다

Self-Validating: 자체적으로 검증이 가능해야 한다

  • 결과값을 수동으로 확인(아래 예시처럼) X
System.out.println(num);
logger.info(num)

Timely: 적시에 작성해야 한다

테스트 종류 - 통합 테스트? 단위 테스트?

참고한 블로그에 너무나도 정리를 잘해주신 표

어노테이션설명부모 클래스Bean
@SpringBootTest통합 테스트, 전체IntegrationTestBean 전체
@WebMvcTest단위 테스트, Mvc 테스트MockApiTestMVC 관련된 Bean
@DataJpaTest단위 테스트, Jpa 테스트RepositoryTestJPA 관련 Bean
None단위 테스트, Service 테스트MockTestNone
NonePOJO, 도메인 테스트None

Service Test

핵심: 테스트하고자 하는 컴포넌트의 기능 자체만 테스트

→ Service가 의존하는 클래스나 써드파티(Redis, Kafka 같은)는 제외

→ 외부 의존성을 줄임

테스트 속도 👍

단점

  • 의존성 있는 객체를 Mocking → 문제가 완결된 테스트 X
  • 해당 테스트만 진행하므로, 실제 환경에서 제대로 동작하지 않을 가능성 O → 외부 의존에 대한 테스트는 통합 테스트에서 진행

Mocking

테스트하고자 하는 컴포넌트 외에는 모두 모조품으로 바꿔치우자 !!

✏️ 예시 코드 - Base 클래스

@RunWith(MockitoJUnitRunner.class)
@ActiveProfiles(TestProfile.TEST)
@Ignore
public class MockTest {

}

MockitoJunitRunner를 통해 Mock 테스트 진행

✏️ 예시 코드 - Base 클래스를 활용한 Test code

public class MemberSignUpServiceTest extends MockTest {

    @InjectMocks
    private MemberSignUpService memberSignUpService;

    @Mock
    private MemberRepository memberRepository;
    private Member member;

    @Before
    public void setUp() throws Exception {
        member = MemberBuilder.build();
    }

    @Test(expected = EmailDuplicateException.class)
    public void 회원가입_이메일중복_경우() {
        //given
        final Email email = member.getEmail();
        final Name name = member.getName();
        final SignUpRequest dto = SignUpRequestBuilder.build(email, name);

        given(memberRepository.existsByEmail(any())).willReturn(true);

        //when
        memberSignUpService.doSignUp(dto);
    }
}

‼️ 주의 → 실제 데이터베이스는 관심 밖이라는 점이다

  • @Test(expected = EmailDuplicateException.class) 이메일이 중복되었을 경우 EmailDuplicateException 예외가 발생하는지 확인

Mock API Test

보통 통합테스트를 진행하기 어려운 테스트를 진행

→ Rollback 처리가 힘들거나 불가능한 테스트 주로 사용(ex. 외부 API)

→ 통합 테스트에서 해당 객체를 Mock 객체로 변경해서 테스트 진행하는 것이 MockAPI

✏️ 예시 코드

@WebMvcTest(MemberApi.class)
public class MemberMockApiTest extends MockApiTest {
    @MockBean private MemberSignUpService memberSignUpService;
    @MockBean private MemberHelperService memberHelperService;
    ...

    @Test
    public void 회원가입_유효하지않은_입력값() throws Exception {
        //given
        final Email email = Email.of("asdasd@d"); // 이메일 형식이 유효하지 않음
        final Name name = Name.builder().build();
        final SignUpRequest dto = SignUpRequestBuilder.build(email, name);
        final Member member = MemberBuilder.build();

        given(memberSignUpService.doSignUp(any())).willReturn(member);

        //when
        final ResultActions resultActions = requestSignUp(dto);

        //then
        resultActions
                .andExpect(status().isBadRequest())
        ;

    }

‼️ 주의 → 테스트 관심사는 오직 Request와 그에 따른 Response

  • @WebMvcTest(MemberApi.class) -> 테스트하고자 하는 클래스 등록
  • @MockBean 으로 객체 주입 받아 Mocking 작업

장점

  • WebApplication 관련된 Bean들만 등록 통합 테스트보다 빠른 속도

단점

  • 요청부터 응답까지 모든 테스트를 Mock 기반으로 테스트하기 때문에 실제 환경에서는 제대로 동작하지 않을 가능성 O

Repository Test

Repository에 대한 관심사만 갖고 하는 테스트

💡 주의

  • JpaRepository에서 기본적으로 제공해주는 findById, deleteById 같은 함수는 테스트 하지 않음
  • save() null 제약 조건
  • 주로 커스텀 쿼리 메서드, @Query으로 작성된 JPQL 등을 테스트

✏️ 예시 코드 - Base 클래스

@RunWith(SpringRunner.class)
@DataJpaTest
@ActiveProfiles(TestProfile.TEST)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Ignore
public class RepositoryTest {
}
  • @DataJpaTest: Repository에 대한 Bean만 등록 기본적으로 메모리 데이터베이스에 대한 테스트
  • @AutoCnofigureTestDatabase : profile에 등록된 데이터베이스 정보로 대체 가능

✏️ 예시 코드 - Base 클래스를 이용한 Test Code

public class MemberRepositoryTest extends RepositoryTest {

    @Autowired
    private MemberRepository memberRepository;

    private Member saveMember;
    private Email email;

    @Before
    public void setUp() throws Exception {
        final String value = "cheese10yun@gmail.com";
        email = EmailBuilder.build(value);
        final Name name = NameBuilder.build();
        saveMember = memberRepository.save(MemberBuilder.build(email, name));
    }

    ...
    @Test
    public void existsByEmail_존재하는경우_true() {
        final boolean existsByEmail = memberRepository.existsByEmail(email);
        assertThat(existsByEmail).isTrue();
    }

    @Test
    public void existsByEmail_존재하지않은_경우_false() {
        final boolean existsByEmail = memberRepository.existsByEmail(Email.of("ehdgoanfrhkqortntksdls@asd.com"));
        assertThat(existsByEmail).isFalse();
    }
}
  • setUp() 을 통해 Member를 데이터베이스에 insert 테스트 코드 실행마다 insert → rollback이 자동 실행
  • 추가 작성한 쿼리 메서드 existsByEmail 테스트 진행 실제로 작성된 쿼리가 어떻게 출력되는지 show-sql 옵션을 통해 확인

Integration Test

Mock, 즉 모조품으로 의존하던 대상들을 실제 Bean으로 바꾸어서 테스트

통합 테스트는 주로 컨트롤러 테스트 진행 → 요청부터 응답까지 전체 플로우 테스트

장점

  • 모든 Bean을 올리고 테스트 진행 → 쉬움, 운영환경과 가장 유사하게 테스트 가능
  • API 테스트 경우 → 요청부터 응답까지 전체적인 테스트 진행 가능

단점

  • 긴 테스트 시간
  • 큰 테스트 단위 → 테스트 실패시 디버깅 곤란
  • 외부 API 콜같은 Rollback 처리가 안되는 테스트 진행 어려움

✏️ 예시코드 - 통합 테스트의 Base 클래스

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApiApp.class)
@AutoConfigureMockMvc
@ActiveProfiles(TestProfile.TEST)
@Transactional
@Ignore
public class IntegrationTest {
    @Autowired protected MockMvc mvc;
    @Autowired protected ObjectMapper objectMapper;
    ...
}
  • @ActiveProfiles(TestProfile.TEST) : 설정으로 테스트에 profile을 지정 → local 또는 docker 이렇게 환경별로 yml 파일을 관리하듯이 test도 별도의 파일로 관리하는 것이 좋음 !!
  • @Transactional : 테스트 코드의 데이터베이스 정보가 자동으로 Rollback (베이스 클래스에 이 속성을 추가해야 실수 없이 진행) → 데이터베이스 상태의존적이지 않을 수 있다.
  • @Ignore : 테스트 실행시 동작할 필요 X

✏️ 예시코드 - 위 Base 클래스를 활용한 통합 테스트

public class MemberApiTest extends IntegrationTest {

    @Autowired
    private MemberSetup memberSetup;

    @Test
    public void 회원가입_성공() throws Exception {
        //given
        final Member member = MemberBuilder.build();
        final Email email = member.getEmail();
        final Name name = member.getName();
        final SignUpRequest dto = SignUpRequestBuilder.build(email, name);

        //when
        final ResultActions resultActions = requestSignUp(dto);

        //then
        resultActions
                .andExpect(status().isOk())
                .andExpect(jsonPath("email.value").value(email.getValue()))
                .andExpect(jsonPath("email.host").value(email.getHost()))
                .andExpect(jsonPath("email.id").value(email.getId()))
                .andExpect(jsonPath("name.first").value(name.getFirst()))
                .andExpect(jsonPath("name.middle").value(name.getMiddle()))
                .andExpect(jsonPath("name.last").value(name.getLast()))
                .andExpect(jsonPath("name.fullName").value(name.getFullName()))
        ;
    }
    private ResultActions requestSignUp(SignUpRequest dto) throws Exception {
        return mvc.perform(post("/members")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(objectMapper.writeValueAsString(dto)))
                .andDo(print());
    }
    ...
}
  • 조회테스트 같은 경우, 테스트 전에 미리 데이터베이스에 insert

중요!! 데이터베이스 상태에 너무 의존적인 테스트는 향후 로직 문제가 아니더라도 테스트가 실패하는 상황이 자주 발생할 수 있음


Regression Test

시스템에 변화가 생겼을 때(새로운 패치, 업그레이드, 버그 수정) 시에 기존에 잘 작동하던 테스트들을 시스템 통합 후에도 여전히 잘 동작하는지 확인하는 테스트

Acceptance Test

실제 고객이나 유저가 SW가 요구사항대로 작동하는지 확인해 보는 과정

참고

profile
한 걸음 한 걸음 쌓아가자😎

0개의 댓글