이번 외주를 진행하며 처음으로 인수 테스트 코드를 작성하게 되었다.
사이드 프로젝트를 진행하며 단위 테스트나 통합 테스트는 진행해 보았지만
인수 테스트는 처음이기에 정리하면서 글로 남겨볼까 한다...
들어가기 앞서 테스트 코드 종류에 대해 알아보고 넘어가자.
예시 코드
@Test
@DisplayName("testName")
void testExample() {
//given
//when
//then
assertThat(object.getXXX()).isEqualTo(xxx);
}
예시 코드
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
public class ExampleTest{
}
예시 코드
// then
mockMvc.perform(
post("/app/example")
.header(HttpHeaders.AUTHORIZATION, jwt)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(exampleRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andDo(print());
이번 외주를 진행하며 확정된 기획들에 한해서 작성된 API들에 대한 인수 테스트를 진행해 달라는 요청을 받았다.
인수 테스트에 대한 여러 레퍼런스 서치를 통해 블랙박스 테스트임을 알고 Junit의 MockBean 어노테이션을 이용하여 서비스 레이어를 Mocking한 뒤 정상 Response를 반환하는지 여부만 검증하는 로직을 구성했다.
@SpringBootTest
public class ExampleTest {
@Autowired
MockMvc mockMvc;
@MockBean
ExampleService exampleService
Example example;
@BeforeEach
void setUp() {
example = new Example();
}
@Test
void exampleTest() {
//given
given(exampleService.retrieve(any())).willReturn(example);
//when
//then
mockMvc.perform(get("/admin/examples/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andDo(print());
}
}
컨펌을 받으러 가보니 인수 테스트이더라도 내부 로직 검증은 필요하다는 리뷰를 받게 되었다.
추가로 테스트 격리를 이용하여 테스트 이후 데이터베이스 초기화까지 필요하다는 리뷰를 받게 되었다.
보스독님의 블로그에서 인수 테스트는 Mock 프레임워크를 사용하지 않고 최대한 실 서비스 환경과 동일한 환경에서 테스트 되어야 한다는 것을 알게 되었다.
서비스 레이어의 Mocking을 제거하고 profile을 분리함으로써 실 서비스 환경과 동일하도록 리팩토링을 진행했다.
추가로 TRUNCATE 쿼리를 통해 각 컨트롤러 레이어 테스트 이후 데이터베이스 초기화를 진행해 줌으로써 격리 수준을 높였다.
@Service
public class AcceptanceTestHelper implements InitializingBean {
@PersistenceContext private EntityManager entityManager;
private List<String> tableNames;
@Override
public void afterPropertiesSet() throws Exception {
tableNames =
entityManager.getMetamodel().getEntities().stream()
.filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null)
.map(
entity -> {
return entity.getName();
})
.toList();
}
@Transactional
public void execute() {
entityManager.flush();
entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate();
tableNames.forEach(
table -> {
entityManager.createNativeQuery("TRUNCATE TABLE " + table).executeUpdate();
});
entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate();
}
}
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class ExampleControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
ExampleRepository exampleRepository;
@Autowired
AcceptanceTestHelper acceptanceTestHelper;
@BeforeAll
void setUp() {
Example example = new Example();
exampleRepository.save(example);
}
@AfterAll
void flushing() throws Exception {
acceptanceTestHelper.afterPropertiesSet();
acceptanceTestHelper.execute();
}
@Test
void example_test() throws Exception {
//given
//when
//then
mockMvc.perform(get("/admin/examples/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andDo(print());
}
}