1️⃣ Test Guide
☝🏻 Spring Boot에서의 테스트
Spring Boot에서 테스트를 어떻게 해야할까요?
Spring Boot는 Spring Application의 테스트를 위해 많은 기능을 지원해줍니다!
🐘 spring-boot-starter-test
spring-boot-starter-test
는 테스트의 주요 종속성입니다. 여기에는 테스트에 필요한 대부분의 요소가 포함되어 있습니다. (JUnit, Mockito 등등…)
의존성을 추가하고 나면 다음과 같은 애플리케이션 테스트 방법들을 사용할 수 있습니다.
종류 | 요약 | Bean 범위 |
---|---|---|
@SpringBootTest | 전체 테스트 어노테이션 | 애플리케이션에 주입된 Bean 전체 |
@WebMvcTest | Controller Layer 테스트 | MVC 관련 Bean (Controller, ControllerAdvice) |
@DataJpaTest | JPA(DB I/O) 테스트 | JPA 관련 Bean (EntityManager) |
@RestClientTest | Rest API 테스트 | RestTemplate 등 일부 Bean |
@JsonTest | Json 데이터 테스트 | Json 관련 일부 Bean |
이중에서 가장 많이 사용되는 테스트는 @SpringBootTest
와 @WebMvcTest
입니다. 때문에 조금 더 자세히 살펴보겠습니다.
- SpringBootTest
- SpringBootTest는 실제 애플리케이션을 자신의 로컬 위에 올려서 포트 주소가 Listening 되어지고, 실제 Database와 커넥션이 붙어지는 상태에서 진행되는 Live 테스트 방법입니다.
- WebMvcTest
- MVC를 위한 테스트입니다. 컨트롤러가 예상대로 작동되는지 테스트하기 위해 사용됩니다.
- Web Layer만 로드하며, @WebMvcTest 어노테이션을 사용시 아래의 항목들만 스캔하도록 제한하여 보다 빠르고 가벼운 테스트가 가능합니다.
ex)
@Controller
,@ControllerAdvice
,@JsonComponent
,@Convert
,Filter
,WebMvcConfigurer
등등… - WebApplication과 관련된 Bean들만 등록하기 때문에 @SpringBootTest보다 빠릅니다. 또한 통합테스트를 진행하기 어려운 테스트를 개별적으로 진행 가능합니다.
언뜻보면 두 테스트간의 차이는 전체 애플리케이션을 띄운다는 점과 일부 Controller만을 띄운다는 점으로 보여지는데, 목적으로만 본다면 이 점이 가장 핵심이며 애플리케이션의 규모가 커지게 되는 경우 테스트 시간이 그만큼 길어지지 때문에 신규 기능이나 버그 패치 등 일부 기능만을 테스트하고자 할 때는 WebMvcTest가 적당하다고 볼 수 있습니다.
🔷 JUnit 훑어보기
-
JUnit이란?
JUnit이란 자바 프로그래밍 언어용 테스트 프레임워크로 어노테이션 기반 테스트를 지원합니다. 또한 단정문을 통해서 테스트 케이스의 기대값에 대해 수행 결과를 확인할 수 있습니다.
-
JUnit Annotation
JUnit에서 가장 많이 사용되는 어노테이션입니다.
@Test
어노테이션과 생명주기에 관련된 어노테이션을 가지고 있습니다.Annotation Description @Test 테스트용 메서드를 표현하는 어노테이션 @DisplayName 테스트 메서드 이름을 지정하는 어노테이션 @BeforEach 각 테스트 메서드가 시작되기 전에, 실행되어야 하는 메소들 표현 @AfterEach 각 테스트 메서드가 시작된 후 실행되어야 하는 메소드르 표현 @BeforeAll 테스트 시작 전에 실행되어야 하는 메서드를 표현 (Static 처리 필요) @AfterAll 테스트 종료 후에 실행되어야 하는 메서드를 표현 (Static 처리 필요) -
Assertions
JUnit에서 지원해주는 대표적인 검증 함수들입니다.
Method Description assertEquals(a, b) a와 b의 값이 동일한지 확인 assertSame(a, b) a와 b의 객체가 동일한지 확인 assertNull(a) a가 null인지 확인 assertNotNull(a) a가 true인지 확인 assertTrue(a) a가 false인지 확인 assertFalse(a) a가 false인지 확인 assertThrows(입셉션 에러 종류 a, 발생하는 로직 b) b 로직 시에 a 인셉션이 발생하는지 확인 assertThat 어떤 조건이 참인지 확인
📂 Persistence Layer
@DataJpaTest
를 사용해 Repository Test를 해봅시다.
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void findEmailById() {
// given
User user = UserDataMaker.getSaveEntity();
User saveUser = userRepository.save();
// when
String email = userRepository.findEmailById(saveUser.getId());
// then
assertEquals(email, saveUser.getEmail());
}
}
@DataJpaTest
는 JPA에 관련된 요소들만 테스트하기 위한 어노테이션으로 JPA 테스트에 관련된 설정들만 적용해줍니다.- 메모리상에 내부 데이터베이스를 생성하고
@Entity
클래스들을 등록하고 JPA Repository 설정들을 해줍니다. 각 테스트마다 테스트가 완료되면 관련한 설정들은 롤백됩니다. - 복잡한 data jpa repository의 함수를 테스트할 때 사용하기 적합합니다.
🤖 Business Layer
@SpringBootTest
사용해 Unit Test를 해봅시다.
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private UserDataMaker userDataMaker;
@Test
void saveSuccess() {
// given
UserSaveRequest request = UserDataMaker.getSaveRequest();
// when
UserSaveResponse result = userService.save(request);
// then
assertEquals(request.name(), result.name());
}
@Test
void updateFail() {
// given
UserUpdateRequest request = UserDataMaker.getUpdateRequest();
// when & then
assertThatThrownBy(() -> userService.update(0, request))
.isInstanceOf(BusinessException.class)
.hasMessage(USER_NOT_EXIST);
}
}
- Unit Test를 할 때,
@SpringBootTest
를 사용합니다.@SpringBootTest
는 🐘 spring-boot-starter-test 에서 설명했으므로 이하 생략합니다. - 추가로
@SpringBootTest
를 사용하지 않고 Mockito를 활용해 테스트를 진행할 수 있습니다. 관련 내용은 아래 게시글을 참고하여 주세요.
🚦 Presentation Layer
@WebMvcTest
를 사용해 MVC Test를 해봅시다.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
@Test
void getAll() throws Exception {
// given
UserSaveRequest request = UserDataMaker.getSaveRequest();
given(userService.save(any())
.willReturn(request);
// when & then
mockMvc.perform(get("/users"))
.contentType(MediaType.APPLICATION_JSON)
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.name").value("user"))
.andExpect(jsonPath("$.data.age").value(21))
.andDo(print());
}
@Test
void save() throws Exception {
// given
UserSaveRequest request = UserDataMaker.getSaveRequest();
// when & then
mockMvc.perform(post("users"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.name").value("user"))
.andExpect(jsonPath("$.data.age").value(21))
.andDo(print());
}
}
MockMvc
- 애플리케이션을 배포하지 않고도, 서버의 MVC 동작을 테스트할 수 있는 라이브러리입니다.
- 주로 Controller 레이어 단위 테스트에 많이 사용됩니다.
- MockMvc 메소드 살펴보기
- perform()
- 요청을 전송하는 역할을 합니다.
- 결과로 ResultActions 객체를 받으며, ResultActions 객체는 리턴 값을 검증하고 확인할 수 있는 andExcpect() 메소드를 제공해줍니다.
- get()
- HTTP 메소드를 결정할 수 있습니다. (get, post, put, delete)
- 인자로는 경로를 보내줍니다.
- params()
- 키=값의 파라미터를 전달할 수 있습니다.
- 여러 개일 때는 params()를, 하나일 때에는 param()을 사용합니다.
- andExpect()
- 응답을 검증하는 역할을 합니다.
- 상태 코드, 뷰, 리다이렉트, 모델 정보, 응답 정보 검증 등등을 할 수 있습니다.
- andDo()
- 요청/응답 전체 메시지를 확인할 수 있습니다.
- perform()
ObjectMapper
- ObjectMapper는 텍스트 형태의 JSON을 object로 변경해 주거나 object를 텍스트 형태의 JSON으로 변경해 주는 것을 의미합니다.
- 예를 들면 컨트롤러에 요청이 오면, Content-Type이 JSON 타입인 것을 object로 변경을 해주고, 처리 후 응답으로 클라이언트에게 자동으로 Object를 요청 형태에 해당되는 json으로 변경해줍니다.
@MockBean
- 말 그대로 가짜 객체입니다.
- 해당 단위 테스트에만 집중할 수 있도록 도와줍니다.
- 여기서는 서비스를 MockBean으로 선언하였고, 서비스 내 의존성 연결고리를 신경쓰지 않아도 되며 서비스의 호출, 결과를 임의로 조작하여 테스트를 지원합니다.
✌🏻 TDD 묻고 BDD로 가!
자꾸 나왔던 given, when, then 궁금하시죠? 이 목차에서 다룹니다.
🐙 TDD란?
https://log-hannah.notion.site/00-TDD-50e14ffbbd2243ec90ffdff44d70f308?pvs=4
🦀 BDD란?
https://makeshop.notion.site/BDD-68296d4d1be3416c828563b2f5d6a129?pvs=4
TDD와 BDD의 차이
TDD - Test Driven Development | BDD - Behavior Driven Development | |
---|---|---|
테스트 코드의 목적은? | 기능 동작의 검증 | 서비스 유저 시나리오 동작의 검증 |
테스트 코드의 설계중심은? | 제공할 모듈의 기능 중심 | 서비스 사용자 행위 중심 |
테스트 코드 설계 재료는? | 모듈 사양 문서 (개발자가 작성) | 서비스 기획서 (서비스 기획자가 작성) |
어떤 프로젝트에 적합한가? | 모듈/라이브러리 프로젝트 | 서비스 프로젝트 |
장점은? | 설계 단계에서 예외 케이스들을 확인할 수 있다 | 설계 단계에서 누락된 기획을 확인할 수 있다 |
BDD와 TDD는 서로 상호보완적인 관계입니다. BDD로 어떠한 행위를 테스트하고 해당 행위에서 깊게 테스트 하기 위해서 TDD가 필요합니다. 때문에 프로젝트에서 혼용하여 사용하는 것을 권장드립니다.
2️⃣ Test Scenario Guide
- findAll Method
- 데이터가 3개 존재할 때
- 리스트 count가 3이어야 한다
- 응답 코드가 200이어야 한다
- 데이터가 존재하지 않을 때
- 리스트 count가 0이어야 한다
- findOne Method
- 저장된 데이터를 단건 조회할 때
- 저장된 데이터와 조회한 데이터가 같아야 한다
- 저장되지 않은 데이터를 단건 조회할 때
- 응답 코드가 404이어야 한다
- ErrorCode는 NOT_EXIST이어야 한다
☝🏻 성공 케이스
모든 API에 대한 정상적인 응답을 테스트합니다.
✌🏻 실패 케이스
- 조회 엔티티가 없는 경우
- 연관 엔티티가 없는 경우
- 바디 or 조회 쿼리가 올바르지 않은 경우
- 그 외 비지니스 로직에서 Exception이 발생하는 경우
👋🏻 @Nested
- BDD 패턴으로 테스트하기 위해서 사용하는 어노테이션입니다.
3️⃣ 환경 통합하기
☝🏻 ControllerTest & DataCleaner
🧐 ControllerTest
@SpringBootTest
@AutoConfigureMockMvc
public abstract class ControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
protected DataCleaner dataCleaner;
@AfterEach
void tearDown() {
dataCleaner.clear();
}
}
ControllerTest
는 E2E 테스트를 위한 상위 abstract class입니다.
ControllerTest
를 상속받으면 상속 받은 클래스에서 E2E 테스트를 위한 필수적인MockMvc
,ObjectMapper
를 선언해주지 않아도 사용할 수 있습니다.- 또한
tearDown
메소드를 활용해 테스트 단위별로 메모리 DB 남아있는 데이터들을 삭제할 수 있습니다. (테스트가 의존적이지 않도록 도움) - 추후 확장의 용이성을 위해서 abstract class로 생성을 하였습니다.
-
(번외)
@AutoConfigureMockMvc
는 뭔가요?@AutoConfigureMockMvc
는@WebMvcTest
와 비슷하게 MockMVC를 제어하는 어노테이션입니다.자세한 사항은 아래 블로그를 참고해주세요 🙂
#3-3_ 스프링부트 테스트코드 작성시 @WebMvcTest 와 @AutoConfigureMockMvc 의 차이점
🫨 DataCleaner
@Component
public class DataCleaner {
private static final String FOREIGN_KEY_CHECK_FORMAT = "SET REFERENTIAL_INTEGRITY %d";
private static final String TRUNCATE_FORMAT = "TRUNCATE TABLE %s";
private final List<String> tableNames = new ArrayList<>();
@PersistenceContext
private EntityManager entityManager;
@PostConstruct
public void findDatabaseTableNames() {
List<Object[]> tableInfos = entityManager.createNativeQuery("SHOW TABLES").getResultList();
for (Object[] tableInfo : tableInfos) {
String tableName = (String) tableInfo[0];
tableNames.add(tableName);
}
}
@Transactional
public void clear() {
entityManager.clear();
truncate();
}
private void truncate() {
entityManager.createNativeQuery(String.format(FOREIGN_KEY_CHECK_FORMAT, 0)).executeUpdate();
for (String tableName : tableNames) {
entityManager.createNativeQuery(String.format(TRUNCATE_FORMAT, tableName)).executeUpdate();
}
entityManager.createNativeQuery(String.format(FOREIGN_KEY_CHECK_FORMAT, 1)).executeUpdate();
}
}
DataCleaner
는 데이터 삭제 지원을 위한 클래스입니다. 엔티티 매니저와 네이티브 쿼리를 사용해 직접 데이터를 삭제합니다.
findDatabaseTableNames()
: 테스트 애플리케이션이 띄워질 때 테이블들(엔티티들)의 이름을 저장해놓습니다.clear()
: 엔티티 매니저의clear()
함수를 호출해 영속성 상태를 준영속성(엔티티들이 영속성 컨텍스트에 저장되었다가 분리된 상태)으로 만들어줍니다.truncate()
: h2 database의 native query를 통해 메모리 DB에 들어있는 데이터들을 직접 삭제해줍니다.
사용 권장 E2E 테스트 시
ControllerTest
클래스를 상속받아서 사용하도록 합니다.
✌🏻 Data Maker
🫠 구조
도메인별 Data Maker 객체 구조는 다음과 같습니다.
@Component
public class CenterDataMaker {
// 도메인 Repository (필요한 Repository)
@Autowired
private CenterRepository centerRepository;
// 필요한 데이터 Repository 함수들
public Center save() {
Center center = getSaveEntity();
return centerRepository.save(center);
}
// ...
// 필요한 데이터 도우미 함수들 (주로 static 함수)
public static Center getSaveEntity() {
return new Center("", "", "");
}
// ...
}
🫥 사용
객체 사용안은 다음과 같습니다.
- 미리 저장해야할 데이터 저장시키기
- 길어지는 데이터 객체 대신 생성해주기
@TestInstance(PER_CLASS)
public class CenterControllerTest extends ControllerTest {
@Autowired
private CenterDataMaker centerDataMaker;
@Test
void saveCenter() throws Exception {
// given
CenterSaveRequest request = **CenterDataMaker.getSaveRequest()**;
// when & then
mockMvc.perform(post("/api/center"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
.andExpect(status().isCreated());
}
@Test
void findList() throws Exception {
// given
List<Center> centers = **centerDataMaker.saveAll(3)**;
// when & then
mockMvc.perform(get("/api/center"))
.andExpect(status().isOk());
}
}