- 단위 테스트(Unit Test): 비즈니스 로직에서 사용하는 각각의 메서드를 독립적으로 테스트한다.
- 슬라이스 테스트: 애플리케이션을 특정 계층으로 쪼개어 하는 테스트로, Mock 객체를 사용한다.
- 통합 테스트: 클라이언트 툴 없이 개발자가 짜 놓은 테스트 코드를 실행시켜서 기대 결과를 확인한다. Controller의 API를 호출하면 서비스 계층과 데이터 액세스 계층을 거쳐 기대 결과를 확인한다.
- 기능 테스트: 사용자 입장에서 애플리케이션의 기능이 올바르게 동작하는지 테스트한다. 일반적으로 테스트 전문 부서에서 진행하며, API 툴이나 데이터베이스까지 연관되어 있다.
🌳 F.I.R.S.T 원칙
- Fast: 테스트 케이스는 빨라야 한다.
- Independent: 각 테스트 케이스는 독립적이어서 서로 다른 테스트에 영향을 주지 않아야 한다.
- Repeatable: 어떤 환경에서도 반복해서 실행이 가능해야 한다.
- Self-validating: 성공 또는 실패라는 자체 검증 결과를 보여 줘야 한다.
- Timely: 테스트하려는 기능 구현을 하기 직전에 작성해야 한다.
JUnit
Java 언어로 만들어진 애플리케이션을 테스트 하기 위한 오픈 소스 테스트 프레임워크이다. 요즘 단위 테스트는 주로 Given - When - Then 패턴의 세 단계로 작성된다.
- `@Test`: JUnit은 테스트 패키지 하위에 해당 애너테이션이 붙은 메서드를 테스트 메서드임을 인식한다.
@DisplayName("회원 정보 조회 테스트")
@Test
void getMemberTest() {
Given
// 테스트에 필요한 전제 조건
// 테스트 대상에 전달되는 입력 값
When
// 테스트 할 동작 지정
Then
// 테스트 결과 검증
// 예상 값과 수행 결과를 비교하여 기대한 동작을 수행하는지 검증(Assertion)
}
🌳 Assertion 메서드
assertEquals(A, B)
assertNotNull(A, "실패 시 표시 메시지")
assertThrows(NullPointerException.class, () -> A("Hello")) // 람다: Executable 함수형 인터페이스
🌳 테스트 실행 전후처리
@BeforeEach // 각 테스트 직전에 실행: 초기화 작업
@BeforeAll // 클래스 레벨에서 한꺼번에 실행할 때, 실행 이전 한 번 초기화 작업 가능: 정적 메서드에 붙일 수 있다
@AfterEach
@AfterAll
@BeforeAll
라이프 사이클을 클래스 단위로 하는 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
를 붙이면
static이 아니어도 사용할 수 있다.
https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/TestInstance
🌳 Assumption: 조건부 테스트
특정 환경에서만 실행되도록 할 수 있다.
// 시스템 운영체제가 Windows라면 아래 나머지 로직이 실행된다
assumeTrue(System.getProperty("os.name").startsWith("Windows"));
Hamcrest
JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework
- Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어져서 가독성이 좋다.
- 테스트 실패 메시지의 가독성이 좋다.
- 다양한 Matcher를 제공한다.
- http://hamcrest.org/JavaHamcrest/tutorial
// JUnit
assertEquals(expected, actual);
// Hemcrest
assertThat(actual, is(equalTo(expected)));
슬라이스 테스트
한 계층 영역에 대한 테스트에 집중하는 것이다. 이때, Mock 객체가 없으면 다른 계층을 넘나들어야 하기 때문에 통합 테스트에 가까워진다.
@SpringBootTest // 스프링 부트 기반 애플리케이션을 테스트 하기 위한 Application Context 생성
@AutoConfigureMockMvc // Controller 테스트를 위한 애플리케이션 자동 구성 작업
public class ControllerTestDefaultStructure {
@Autowired
private MockMvc mockMvc; // 서버 실행 없이 Controller 테스트 환경 지원
@Autowired
private Gson gson;
@Test
public void postMemberTest() {
// given: 테스트용 request body 생성
// when: MockMvc 객체로 테스트 대상 Controller 호출
// then: Controller 핸들러 메서드에서 응답으로 수신한 HTTP Status 및 response body 검증
}
}
🌳 API 계층 테스트
- given: 테스트용 Request Body 생성
- when: MockMvc 객체로 테스트 대상 Controller 호출
- then: Controller 핸들러 메서드의 응답인 HTTP Status와 Request Body 검증
@Test
void postMemberTest() throws Exception {
// given
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1234-5678");
String content = gson.toJson(post); //JSON 변환 라이브러리
// when
ResultActions actions =
mockMvc.perform( // Controller의 핸들러 메서드에 요청 전송
post("/v11/members")
.accept(MediaType.APPLICATION_JSON) // 클라이언트가 리턴받을 응답 타입
.contentType(MediaType.APPLICATION_JSON) // 서버에서 처리 가능한 타입
.content(content) //Request Body 데이터
);
// then
MvcResult result = actions // perform()의 반환 타입으로 Request 검증 가능
.andExpect(status().isCreated())
.andExpect(jsonPath("$.data.name").value(post.getName()))
.andReturn(); // Response 데이터 확인 가능, 디버깅 용도로 응답 데이터 출력 시 사용
// System.out.println(result.getResponse().getContentAsString());
}
🌳 데이터 액세스 계층 테스트
테스트 후에는 데이터베이스 상태를 실행 이전으로 되돌려서 깨끗하게 만드는 것이 좋다.
`@DataJpaTest`: `@Transactional` 애너테이션을 포함하여 한 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터가 Rollback 처리된다.
@DataJpaTest
public class MemberRepositoryTest {
@Autowired
private MemberRepository memberRepository;
@DisplayName("save()가 잘 동작하는지 확인")
@Test
public void saveMemberTest() {
// given
Member member = new Member();
member.setEmail("hgd@gmail.com");
member.setName("홍길동");
member.setPhone("010-1111-2222");
// when
Member savedMember = memberRepository.save(member);
// then
assertNotNull(savedMember);
assertTrue(member.getEmail().equals(savedMember.getEmail()));
assertTrue(member.getName().equals(savedMember.getName()));
assertTrue(member.getPhone().equals(savedMember.getPhone()));
}
@DisplayName("findByEmail()이 잘 동작하는지 확인")
@Test
public void findByEmailTest() {
...
}
}
Mockito
Spring Framework 자체적으로 지원하고 있는 Mocking 라이브러리이다. 테스트하고 싶은 대상을 다른 영역으로부터 단절시켜서 테스트 대상 계층에만 집중할 수 있도록 한다.
🌳 컨트롤러 핸들러 메서드
- `@MockBean`: Application Context에 등록된 Bean에 대한 Mock 객체를 생성하고 주입한다.
- Stubbing: 테스트를 위해 Mock 객체가 항상 일정한 동작을 하도록 지정한다.
- `Mockito.any( ... )`: 파라미터에 `createMember()`의 파라미터 타입을 넣는다.
- `willReturn( ... )`: `createMember()`가 리턴 할 Stub 데이터이다.
@Autowired
private MockMvc mockMvc;
@Autowired
private Gson gson;
@MockBean // Application Context의 Bean에 대한 Mock 객체 생성, 주입
private MemberService memberService;
@Autowired // MockMemberService(가칭)의 메서드에서 리턴하는 Member를 리턴하기 위해 사용한다
private MemberMapper mapper;
@Test
void postMemberTest() throws Exception {
// given : 테스트용 Request Body 생성
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1234-5678");
Member member = mapper.memberPostToMember(post);
member.setStamp(new Stamp());
// Mockito의 Stubbing 메서드
given(memberService.createMember(Mockito.any(Member.class)))
.willReturn(member);
String content = gson.toJson(post);
더보기
// MemberController.java
// 진짜 로직에서는 service를 거친다!
Member createdMember = memberService.crateMember(member);
// MemberControllerMockTest.java
// 테스트에서는 위 코드가 아닌 아래 코드가 실행되어 createMember에 담긴다!
// stubbing
given(memberService.createMember(Mockito.any(Member.class)))
.willReturn(member);
// when : MvcMock 객체로 테스트 대상 Controller 호출
ResultActions actions =
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);
// then
MvcResult result = actions
.andExpect(status().isCreated())
.andExpect(jsonPath("$.data.email").value(post.getEmail()))
.andExpect(jsonPath("$.data.name").value(post.getName()))
.andExpect(jsonPath("$.data.phone").value(post.getPhone()))
.andReturn();
// System.out.println(result.getResponse().getContentAsString());
}
🌳 서비스 계층 메서드
- `@Mock`: 해당 필드 객체를 Mock 객체로 생성한다.
- `@InjectMocks`: 생성한 Mock 객체를 해당 필드에 주입해 준다.
@ExtendWith(MockitoExtension.class) // Spring을 사용하지 않고 Mockito 기능 사용
public class MemberServiceMockTest {
@Mock
private MemberRepository memberRepository;
@InjectMocks
private MemberService memberService;
@Test
public void createMemberTest() {
// given
Member member = new Member("hgd@gmail.com", "홍길동", "010-1111-1111");
given(memberRepository.findByEmail(member.getEmail()))
.willReturn(Optional.of(member));
// when
// then
assertThrows(BusinessLogicException.class, () -> memberService.createMember(member));
}
'Back-End > Spring' 카테고리의 다른 글
[Spring Boot] 메일 발송하기(Google SMTP) (0) | 2022.09.17 |
---|---|
[Spring MVC] 애플리케이션 빌드 / 실행 / 배포 (0) | 2022.09.16 |
ApplicationEventPublisher: How to use events in Spring | @Async (0) | 2022.09.05 |
트랜잭션 (0) | 2022.09.05 |
비즈니스 예외 던지기 및 예외 처리 (0) | 2022.08.24 |
댓글