Spring - 내가 짜는 Controller Test가 Hybrid Test?

Spring 기반에서 Controller 테스트를 작성시 요청을 보내고, 응답을 확인하는 형태로 많이 작성한다.
그러다 보니 MockMvc나 jsonPath를 조합해서 Controller 테스트를 작성하는 경우가 많다.
이번 포스팅은 MockMvc 기반의 Controller 테스트 작성방식을 다른 관점에서 볼 수 있는 경험담이다.

1. Hybrid Testing?

Thought works에서 일하다 과거 같이 일하게 된 Kris가 프로젝트에 작성된 테스트케이스들에 대해 코드리뷰 해준 경험이 있다.
기존의 Controller 테스트는 MockMvc를 이용해서 요청을 보내고 응답을 받는 방식으로 테스트 케이스를 작성하고 있었다.
그런데 Kris가 너희가 짜고 있는 Controller 테스트는 Hybrid Test에 가깝다는 말을 했다.
그리고 Controller 테스트를 pure한 Unit 테스트로 바꿔보는 것이 어떠겠냐고 제안을 했다.

Hybrid Testing 정의

Hybrid 테스트라는 단어를 들었을때, 단순히 어휘만 보고 뜻을 유추할 뿐 어떤 의미인지 정확하게 알지 못했다. 영어가 짧아 Kris와 깊은 대화를 하지 못했지만, 정의를 보고 어떤 의미에서 그렇게 말했는지 유추해보았다.
첨부한 주소에 있는 내용을 기반으로 “Hybrid Testing”에 대해 정리를 해보았다.

#Hybrid Testing의 정의

  • 하이브리드 테스트는 하향식 및 상향식 접근 방식의 장점을 사용하며, 테스트는 두 기술을 모두 사용해서 수행한다.
  • 레이어 구성 = 상위 레이어 + 기본 대상 레이어 + 하위 레이어
  • 테스트는 중간 레벨 대상 계층에 중점을 두고 진행한다.

장점

  • 개발주기의 초기 단계에서 기본적으로 동작하는 시스템을 만들수 있도록, 가능한 빨리 동작하는 버전의 프로그램을 테스트하고 싶은 경우 채택한다.
  • 코드 커버리지의 정확성이 증가되고, 정기적인 시간 간격을 두고 최소의 테스트 케이스를 생성한다.

테스트할 내용

  • 기능 테스트: 모든 기능이 예상대로 작동하는지 확인
  • 호환성 테스트
  • 연결 테스트: 응용 프로그램이 여러 유형의 네트워크 연결에서 올바르게 작동하는지 확인

2. Spring Controller Test

Spring MVC에서 MockMvc를 이용하여 Controller 테스트를 작성하는 방식이 왜 Hybrid 테스트가 되는지 확인해보자.
#Spring MVC

테스트 범위

  • 실제로 테스트를 하려고 하는 대상(subject)는 컨트롤러의 로직이다.
  • 테스트를 동작방식이 상위 레이어를 통과하고, 하위 레이어를 다 통과해서 테스트를 한다.
    • SpringRunner를 통해서 아래의 어노테이션과 함께 테스트를 수행한다
    • @SpringBootTest : 전체 Application Context 로드가 필요한 경우
    • @WebMvcTest: 컨트롤러에 필요한 Context만 필요한 경우
  • 상위 레이어
    • Filter, Interceptor, DispatcherServlet 포함한 Front controller 수행
    • Request의 항목을 Object로 Deserialize 수행
    • 하위 레이어를 거친 결과가 Response로 리턴 (response body, statusCode를 체크한다)
  • 하위 레이어
    • Controller에서 호출하는 Service 또는 Repository를 포함한 DI(Dependency Injection)를 통
      해서 사용하는 Bean객체들은 @MockBean 어노테이션을 통해서 Mock 객체로 사용
    • 필요한 경우 Mock 객체에 Method의 호출 결과를 의도한 결과로 동작하게끔 준비한다. (Stub == canned answer)

장점

  • 검증하고자 하는 부분은 Controller 로직이지만, API가 실제 동작하는 방식으로 체크를 하기 때문에 테스트 정확도가 더 높다.
  • 개발단계에서 Test Case를 작성하고 수행함으로써 오류를 미리 파악하다.
    (여기서 미리 파악 가능하다는 것은, 로컬에서 어플리케이션을 수행하거나, 배포하기 전에 파악이 가능한 것을 의미한다)

Functional Test 도입

부르는 명칭이 조금씩 다르기는 했지만 Integration Test, API Test, Functional Test라 불렀던 것 같다.
물론 각 명칭마다 조금씩 차이가 있을 수 있지만 동작방식은 API 요청을 보내고, 실제 비지니스 로직과 DB를 거친 결과를 확인하는 방식이다.
Functional Test와 Unit Test는 작성하는 목적은 다르고, 지속하기 위해서는 상당한 노력이 필요하다.

  • 데이터 유효성 처리
    • Embedded DB를 도입하던가 @Transactional을 통해 데이터 유효성을 유지하던가등 Unit Test를 작성할때 하지 않았던 것들을 고민해야한다.
  • 쉽게 테스트 작성하기
    • MockMvc와 jsonPath를 이용해 Functional Test를 작성하기 위해서는 입력값과 결과값을 작성하기 번거롭다.
    • 테스트를 지속적으로 작성하기 위해서는 작성이 쉬워야 하며, Functional Test의 경우 입력과 결과값을 최대한 단순하게 작성하는 것이 도움이 된다.
    • Rest Assured를 개인적으로 추천한다. Request와 Response를 json 기반으로 작성이 가능하
      고, 그 기반으로 Response 검증이 가능하다.

Functional Test vs Hybrid Controller Test

팀의 성숙도와 상황에 따라 Controller Test를 어떻게 작성할 것인지 결정하면 될 것 같다.
이는 개인적인 경험에 따른 판단이기 때문에 다른분과 의견이 다를 수도 있습니다.

  • Hybrid Controller Test 언제 적합할까?

    • 기존의 MockMvc를 통한 Controller Test 수행하면, 상위 레이어와 하위 레이어 검증이 가능하다.
    • 팀의 규모가 작은 경우
    • 팀의 테스트 케이스 작성 및 유지보수의 성숙도가 낮은 경우
    • 비지니스 로직이 단순하거나, API의 값 검증의 가성비가 낮은 경우
    • 기존의 테스트 방식으로 비슷한 효과를 낼 수 있기 때문에 유지하는 것이 현실적일 수 있다.
  • Functional Test 도입 + Controller Test를 Unit Test로 변경은 언제 적합할까?

    • Functional Test를 도입하면 상위 레이어의 기능 검증은 Functional Test로 위임이 가능하고,
      기존 Controller Test처럼 중복해서 검증할 필요없기 때문에 Pure한 Unit Test를 작성할 수 있다.
    • 이말은 Controller 테스트또한 Service 테스트처럼, ApplicationContext를 생성할 필요없이
      MockitoRunner를 이용해서 테스트 작성이 가능한 것을 의미한다.
    • 실제 API의 결과를 검증하는 것이기 때문에 비지니스 로직과 DB를 거친 값을 검증한다.
    • API의 비지니스 로직이 복잡하고, API의 값 검증이 중요한 경우
    • 팀의 규모가 어느 정도되고, 테스트 케이스 작성 및 유지보수의 성숙도가 큰 경우

결론

Kris의 제안대로 기존의 Controller Test 검증방식 중, 상위 레이어의 검증 대상을 Functional Test
로 위임하고, 기존의 Controller 테스트를 Service 테스트처럼 작성하는 것이 옳은 것일까?
결론은 테스트케이스를 작성하고 유지하는 팀의 성숙도에 따라 결정하면 될 것 같다. 또한 팀원들이
기존의 테스트가 커버하지 못하는 부분이 있어 불안함과 부족함을 느낀다면 그때 시도해보는 것도 추천한다.

Share