먼저 테스트를 알기 전에 좋은 테스트를 위한 5가지 원칙을 알아보자!
Fast: 빠르게 수행되어야 한다
Isolated: 독립적으로 수행되어야 한다
Repeatable: 반복적으로 수행해도 결과가 같아야 한다
Self-Validating: 자체적으로 검증이 가능해야 한다
System.out.println(num);
logger.info(num)
Timely: 적시에 작성해야 한다
참고한 블로그에 너무나도 정리를 잘해주신 표
어노테이션 | 설명 | 부모 클래스 | Bean |
---|---|---|---|
@SpringBootTest | 통합 테스트, 전체 | IntegrationTest | Bean 전체 |
@WebMvcTest | 단위 테스트, Mvc 테스트 | MockApiTest | MVC 관련된 Bean |
@DataJpaTest | 단위 테스트, Jpa 테스트 | RepositoryTest | JPA 관련 Bean |
None | 단위 테스트, Service 테스트 | MockTest | None |
None | POJO, 도메인 테스트 | None |
핵심: 테스트하고자 하는 컴포넌트의 기능 자체만 테스트
→ Service가 의존하는 클래스나 써드파티(Redis, Kafka 같은)는 제외
→ 외부 의존성을 줄임
→ 테스트 속도 👍
단점
테스트하고자 하는 컴포넌트 외에는 모두 모조품으로 바꿔치우자 !!
@RunWith(MockitoJUnitRunner.class)
@ActiveProfiles(TestProfile.TEST)
@Ignore
public class MockTest {
}
MockitoJunitRunner를 통해 Mock 테스트 진행
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 예외가 발생하는지 확인보통 통합테스트를 진행하기 어려운 테스트를 진행
→ 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 작업장점
단점
Repository에 대한 관심사만 갖고 하는 테스트
💡 주의
- JpaRepository에서 기본적으로 제공해주는
findById
,deleteById
같은 함수는 테스트 하지 않음- save() null 제약 조건
- 주로 커스텀 쿼리 메서드, @Query으로 작성된 JPQL 등을 테스트
@RunWith(SpringRunner.class)
@DataJpaTest
@ActiveProfiles(TestProfile.TEST)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Ignore
public class RepositoryTest {
}
@DataJpaTest
: Repository에 대한 Bean만 등록 기본적으로 메모리 데이터베이스에 대한 테스트@AutoCnofigureTestDatabase
: profile에 등록된 데이터베이스 정보로 대체 가능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
옵션을 통해 확인Mock, 즉 모조품으로 의존하던 대상들을 실제 Bean으로 바꾸어서 테스트
통합 테스트는 주로 컨트롤러 테스트 진행 → 요청부터 응답까지 전체 플로우 테스트
장점
단점
@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
: 테스트 실행시 동작할 필요 Xpublic 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());
}
...
}
중요!! 데이터베이스 상태에 너무 의존적인 테스트는 향후 로직 문제가 아니더라도 테스트가 실패하는 상황이 자주 발생할 수 있음
시스템에 변화가 생겼을 때(새로운 패치, 업그레이드, 버그 수정) 시에 기존에 잘 작동하던 테스트들을 시스템 통합 후에도 여전히 잘 동작하는지 확인하는 테스트
실제 고객이나 유저가 SW가 요구사항대로 작동하는지 확인해 보는 과정
참고