[Spring-Boot] 인수 테스트

김민준·2023년 9월 7일
1

외주

목록 보기
1/1

이번 외주를 진행하며 처음으로 인수 테스트 코드를 작성하게 되었다.
사이드 프로젝트를 진행하며 단위 테스트나 통합 테스트는 진행해 보았지만
인수 테스트는 처음이기에 정리하면서 글로 남겨볼까 한다...

테스트 코드의 종류

들어가기 앞서 테스트 코드 종류에 대해 알아보고 넘어가자.

Unit Test (단위 테스트)

  • 응용 프로그램에서 테스트 가능한 가장 작은 소프트웨어를 실행하여 예상대로 동작하는지 확인하는 테스트
  • 최대한 테스트 범위를 좁혀 디버깅에 유리하게 작성한다.
  • 일반적으로 응용 프로그램의 method, class 단위를 기준으로 한다.
  • 개발자 관점에서 진행하는 화이트박스 테스트
  • Spring Boot에선 Junit 기반 테스트 코드를 작성한다.

예시 코드

@Test
@DisplayName("testName")
void testExample() {

//given

//when

//then
assertThat(object.getXXX()).isEqualTo(xxx);
}

Integration Test (통합 테스트)

  • 응용 프로그램의 모든 구성 요소가 예상대로 함께 작동하는지 확인하는 소프트웨어 테스트
  • 일반적으로 써드 파티나 DB 접근과 같은 개발자가 작성한 코드 외의 영역들도 함께 검증한다.
  • 단위 테스트보다 범위가 넓기 때문에 디버깅이 어려울 수 있다.
  • Spring Boot에선 @SpringBootTest 어노테이션을 통해 작성한다.

예시 코드

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
public class ExampleTest{

}

Acceptance Test (인수 테스트)

  • 실제 사용자들이 해당 프로그램을 사용하는 시나리오를 구성하여 수행하는 테스트
  • 소프트웨어 공학에서 얘기하는 애자일 프로세스의 유저 시나리오 테스트
  • 기본적으로 개발자 관점이 아니기 때문에 블랙박스 테스트이다.
  • 클라이언트의 요구 사항 명세서 대로 작동하는지 확인하는 테스트이다.
  • Spring Boot에선 MockMvc와 같은 유틸 클래스를 이용하여 작성한다.

예시 코드

// 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들에 대한 인수 테스트를 진행해 달라는 요청을 받았다.

AS-IS

인수 테스트에 대한 여러 레퍼런스 서치를 통해 블랙박스 테스트임을 알고 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());
    }

}

Issue

컨펌을 받으러 가보니 인수 테스트이더라도 내부 로직 검증은 필요하다는 리뷰를 받게 되었다.
추가로 테스트 격리를 이용하여 테스트 이후 데이터베이스 초기화까지 필요하다는 리뷰를 받게 되었다.

TO-BE

보스독님의 블로그에서 인수 테스트는 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());
    }
}
profile
기록하는 개발자가 되자

0개의 댓글