먼저 테스트들의 종류부터 확인하고 시작하겠습니다. 다음 그림을 통해 각 테스트가 맡고 있는 역할을 짐작할 수 있을 것입니다.
# 기능 테스트
기능 테스트는 테스트 중 가장 큰 단위를 가지고 있는 테스트로, 주로 애플리케이션을 사용하는 사용자 입장에서 애플리케이션이 제공하는 기능이 올바르게 동작하는지를 테스트합니다.
기능 테스트의 경우 API 툴이나 데이터베이스까지 연관되어 있어서 HTTP 통신도 해야되고, 데이터베이스 연결도 해야 되며 그림 상에는 보이지 않지만 외부 서비스와도 연동될 수 있기때문에 얽혀있는 것이 많은 테스트라고 볼 수 있습니다.
# 통합 테스트
기능 테스트는 테스트를 하는 주체가 주로 개발자 이외의 제 3자인 반면에 통합 테스트는 애플리케이션을 만든 개발자 또는 개발팀이 테스트의 주체가 되는것이 일반적입니다.
예를 들어, 개발자가 Controller의 API를 호출하는 테스트 코드를 작성한 후 실행하면 서비스 계층과 데이터 액세스 계층을 거쳐 DB에 실제로 접속해서 기대했던 대로 동작을 하는지 테스트 하는 것은 통합 테스트의 하나라고 볼 수 있습니다.
# 슬라이스 테스트
슬라이스 테스트는 애플리케이션을 특정 계층으로 쪼개어서 하는 테스트를 의미합니다. 즉, API 계층, 서비스 계층 등 각각 하나의 계층들이 슬라이스 테스트의 주체입니다.
# 단위 테스트
단위 테스트 코드는 메서드 단위로 대부분 작성된다고 생각하면 될 것 같습니다.
DB를 사용한다면 단위 테스트라고 보기 힘든것일까?
통합 테스트나 슬라이스 테스트에서 데이터베이스와 연동된다고 해서 무조건적으로 단위 테스트라고 부르기 어렵다기 보다는 데이터베이스의 상태가 테스트 이 전과 이 후가 동일하게 유지될 수 있다면 데이터베이스가 연동된다고 해도 단위 테스트에 포함될 수 는 있습니다.
하지만 일반적으로 단위 테스트는 최대한 독립적인 것이 좋고, 최대한 작은 단위인 것이 더 좋습니다.
더 작은 단위일수록 다른 연관된 기능들을 생각할 필요도 없고, 테스트 코드 짜기도 더 단순해지고 그만큼 빠르게 테스트를 수행할 수 있기때문입니다.
[단위 테스트를 위한 F.I.R.S.T 원칙]
- Fast(빠르게)
일반적으로 작성한 테스트 케이스는 빨라야 한다는 의미입니다. 너무 느려서 돌리기 힘들다면 테스트 케이스를 작성하는 의미가 퇴색될 것입니다.
- Independent(독립적으로)
각각의 테스트 케이스는 독립적이어야 한다는 의미입니다.
단위 테스트는 어떤 테스트 케이스를 먼저 실행시켜도 실행되는 순서와 상관없이 정상적인 실행이 보장 되어야 합니다.
예를 들어, A라는 테스트 케이스를 먼저 실행시킨 후에 다음으로 B라는 테스트 케이스를 실행시켰더니 테스트에 실패하게 된다면 테스트 케이스끼리 독립적이지 않은 것입니다.
- Repeatable(반복 가능하도록)
테스트 케이스는 어떤 환경에서도 반복해서 실행이 가능해야 된다는 의미입니다. 어떤 로컬 환경이나 서버 환경에서 실행하든 반복해서 같은 결과를 확인할 수 있어야 합니다.
- Self-validating(셀프 검증이 되도록)
단위 테스트는 성공 또는 실패라는 자체 검증 결과를 보여주어야 한다는 의미입니다.
즉, 테스트 케이스 스스로 결과가 옳은지 그른지 판단할 수 있어야 한다는 것입니다.
- Timely(시기 적절하게)
단위 테스트는 테스트 하려는 기능 구현을 하기 직전에 작성해야 한다는 의미입니다.
너무 많은 구현 코드가 작성된 상태에서 테스트 케이스를 작성하려면 오히려 테스트 케이스를 작성하는데 더 많은 시간을 들일 가능성도 있습니다.
[참고]
# Given-When-Then 표현 스타일
- Given
- 테스트를 위한 준비 과정을 명시할 수 있습니다.
- 테스트에 필요한 전제 조건들이 포함된다고 보면 됩니다.
- 테스트 대상에 전달되는 입력 값(테스트 데이터) 역시 Given에 포함됩니다.
- When
- 테스트 할 동작(대상)을 지정합니다.
- 단위 테스트에서는 일반적으로 메서드 호출을 통해 테스트를 진행하므로 한두줄 정도로 작성이 끝나는 부분입니다.
- Then
- 테스트의 결과를 검증하는 영역입니다.
- 일반적으로 예상하는 값(expected)과 테스트 대상 메서드의 동작 수행 결과(actual) 값을 비교해서 기대한대로 동작을 수행하는지 검증(Assertion)하는 코드들이 포함됩니다.
public class StampCalculatorTestWithoutJUnit {
public static void main(String[] args) {
calculateStampCountTest();
}
private static void calculateStampCountTest() {
// given
int nowCount = 5;
int earned = 3;
// when
int actual = StampCalculator.calculateStampCount(nowCount, earned);
int expected = 7;
// then
System.out.println(expected == actual);
}
}
이제 본격적으로 단위 테스트를 시작해 보겠습니다. 먼저 테스트 파일을 먼저 말들어야겠죠?
여기서 활용할 수 있는 단축기가 하나 있는데, 자신이 테스트하고 싶은 클래스로 가서 해당 클래스 이름에 커서를 맞추고 Ctrl + Shift + t 를 누르면 자동으로 테스트 파일을 생성해 줍니다.
import org.junit.jupiter.api.Test;
public class JunitFrame {
@Test
public void test1() {
// 테스트 하고자 하는 대상에 대한 테스트 로직 작성
}
@Test
public void test2() {
// 테스트 하고자 하는 대상에 대한 테스트 로직 작성
}
@Test
public void test3() {
// 테스트 하고자 하는 대상에 대한 테스트 로직 작성
}
}
위 코드가 단위 테스트를 진행하기 위한 기본 구조입니다. 각각의 메서드는 테스트 케이스이며 위에 @Test 에노테이션이 붙어 있는 것이 특징입니다.
이제 Junit 테스트에서는 Assertions 메서드를 지원하는데 몇개의 예시를 보고 학습해 보겠습니다.
*assertEquals()
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HelloJUnitTest {
@Test
public void assertionTest() {
String expected = "Hello, JUnit";
String actual = "Hello, Test";
assertEquals(expected, actual);
}
}
위 테스트에 나오는 assertEquals() 메서드는 기댓값과 실제값을 가정하여 각각 넣어주고 해당 값들이 서로 같은지 비교하는 메서드입니다.
현재 변수로 지정한 실제값과 기댓값이 다르기 때문에 테스트를 실행한다면 둘은 같이 않다는 메시지가 출력될 것입니다.
위 결과처럼 어떤 부분에서 failed가 발생했는지 자세하게 출력됩니다.
*assertNotNull()
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class AssertionNotNullTest {
@Test
public void assertNotNullTest() {
String currencyName = "ETH";
assertNotNull(currencyName, "should be not null");
}
}
assertNotNull()은 안에 들어가는 갑이 null이면 failed를 발생시키는 메서드입니다. 위 예시 두가지 이외에도 수많은 Assertions 메서드가 존재합니다.
다음은 슬라이스 테스트를 몇 가지의 예시들과 함께 학습해 보겠습니다.
[API 계층 테스트]
import com.codestates.member.dto.MemberDto;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerTest2 {
@Autowired
private MockMvc mockMvc;
@Autowired
private Gson gson;
@Test
void postMemberTest() throws Exception {
// given
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
"홍길동",
"010-1234-5678");
String content = gson.toJson(post);
// when
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())) // (1)
.andExpect(jsonPath("$.data.name").value(post.getName())) // (2)
.andExpect(jsonPath("$.data.phone").value(post.getPhone())) // (3)
.andReturn();
System.out.println(result.getResponse().getContentAsString());
}
}
코드를 보시면 굉장히 생소한 코드들이 많이 보이실 것입니다. 하나하나 간략히 설명하자면 다음과 같습니다.
- @SpringBootTest - Spring Boot 기반의 애플리케이션을 테스트 하기 위한 Application Context를 생성합니다.
- @AutoConfigureMockMvc - Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 해줍니다. 아래에 MockMvc 객체 등을 활용하기 위해서 꼭 필요합니다.
- MockMvc -Spring 기반의 애플리케이션을 Tomcat 같은 서버를 실행하지 않고, Controller를 테스트할 수 있는 완벽한 환경을 지원해주는 일종의 Spring MVC 테스트 프레임워크입니다.
- Gson - JSON 변환 라이브러리를 이용해서 생성한 MemberDto.Post 객체를 JSON 포맷으로 변환 해줍니다.
- Mockmvc.perform - MockMvc로 테스트 대상 Controller의 핸들러 메서드에 요청을 전송하는 작업을 수행하기 위해서는 기본적으로 perform() 메서드를 호출해야 하며 perform() 메서드 내부에 Controller 호출을 위한 세부적인 정보들이 포함됩니다.
- andExpect - 파라미터로 입력한 매처(Matcher)로 예상되는 기대 결과를 검증할 수 있습니다.
- jsonPath- JSON 형식의 각 프로퍼티 중에서 응답으로 전달 받는 값이 각각 일치하는지 검증할 수 있습니다.
- andReturn - response 데이터를 확인할 수 있는데, 디버깅 용도로 response로 전달되는 응답 데이터를 출력할 때 사용할 수 있습니다
[데이터 엑세스 계층 테스트]
import com.codestates.member.entity.Member;
import com.codestates.member.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest // (1)
public class MemberRepositoryTest {
@Autowired
private MemberRepository memberRepository; // (2)
@Test
public void saveMemberTest() {
// given (3)
Member member = new Member();
member.setEmail("hgd@gmail.com");
member.setName("홍길동");
member.setPhone("010-1111-2222");
// when (4)
Member savedMember = memberRepository.save(member);
// then (5)
assertNotNull(savedMember); // (5-1)
assertTrue(member.getEmail().equals(savedMember.getEmail()));
assertTrue(member.getName().equals(savedMember.getName()));
assertTrue(member.getPhone().equals(savedMember.getPhone()));
}
}
- @DataJpaTest
MemberRepository의 기능을 정상적으로 사용하기 위한 Configuration을 Spring이 자동으로 해주게 됩니다.
@DataJpaTest 애너테이션은 @Transactional 애너테이션을 포함하고 있기 때문에 하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 처리 됩니다.
즉, 여러 개의 테스트 케이스를 한꺼번에 실행 시켜도 하나의 테스트 케이스가 종료될 때마다 데이터베이스의 상태가 초기 상태를 유지한다는 것입니다.
'JAVA > Test' 카테고리의 다른 글
[자바 Spring] 가독성 좋은 테스팅 기법 Hamcrest의 기초 사용 방법 (0) | 2022.07.16 |
---|---|
[Spring 자바] Junit 테스트 순서 지정하는 방법 (1) | 2022.07.14 |