콘텐츠로 건너뛰기

@SpringBootTest + mockMvc vs @WebMvcTest

테스트를 작성하려고 했더니, 아래와 같은 오류가 발생했다.

처음에는 왜 Bean을 인식하지 못하는지,

해당 Bean의 선언과 Configuration에서 Bean Scan Scope를 확인했는데 모두 정상이었다.

나중에 알게된 사실1 인데, 같이 일하는 사람과 서로 테스트의 방식이 달랐던 것이다.

평상시에 내가 제일 즐겨하는 controller 테스트는 아래와 같은 방식이다.

사실 위의 테스트는 SpringBootTest로,

Controller를 실제 동작하는 것과 같이 Integration Test하는 것이다.

옛날부터 MockMvc 테스트를 하다보니, Annotation을 지원하기 전에 사용하던 것이 습관이 된 것이다.

그리고 이와 비슷한 방식으로 Annotation 방식으로 바꾸면,

이렇게 된다.

그리고 오늘 내가 수정하려고 했던 테스트는,

이렇게 @WebMvcTest를 사용하고 있었다.

이 경우는 Mvc에 대해서만 Sliced Test를 제공한다.

즉, Controller에서 관리하는 영역만 Spring Context로 처리하는 것처럼 하고

(예를 들어, filter, session, cookie 등)

이후 연결된 service, repository dependency는 모두 mock으로 처리한다.

그래서 연관된 Bean들이 있다면 @MockBean으로 선언하고 해당 mock의 stub을 만들어줘야 한다.

방금 케이스는 @MockBean이 아니라 @Autowired를 사용하려고 했기때문에 오류가 발생한 것이다.

 

얘기가 나온 김에 Controller Test에 대해서 조금만 더 살펴보자.

위의 1번과 2번은 SpringBoot 컨텍스트를 이용한 테스트로 Integration Test이다.

즉, 특별히 설정하지 않으면

모두 정상적인 Bean을 로딩해서 Controller부터 Service, Repository를 거쳐 DB까지 데이터를 모두 훑어온다.

 

내가 생각하는 장점은,

1) 컨트롤러에서 어떤 요청과 결과가 나가는지 명확하게 테스트로 확인되기 때문에, 내부 로직을 리팩토링하거나 스펙을 추가했을 경우에도 외부에 동일한 결과가 나가는지 확인할 수 있는 좋은 방법이 된다. 특히 외부에서 public rest api를 요청하는 것은 어떻게 요청하고 있을지 몰라서 실제 개발을 할 때 장애의 요소가 되기도 한다. 예를 들어, 어떤 서비스로직에서 null을 허용하다가 null이 들어올 경우 exception을 일으키기로 결정했다면, 명확하게 어떤 이펙트가 발생할지 눈으로 확인이 가능해진다.

2) 비슷한 얘기지만, 각종 Validator에서 Return하는 오류의 값이 예상되지 않는 경우가 있다. Spring Context에서 throw하는 경우도 있고, Hibernate Validator 혹은 Custom Validator에서 throw를 하는 경우도 있는데… 대부분은 Aop의 형식으로 처리되기 때문에, 결과가 어떤 오류가 발생해서 어떤 Response가 나갈지 예상이 어려운 경우가 대부분이다. 이럴 때도 mock이 아니라 실제 context를 사용하기 때문에 명확하게 확인할 수 있다.

 

내가 생각하는 단점은,

1) Controller 테스트가 Unit Test가 아니라 Integration Test가 되기 때문에, 로직변경에 대해서 민감해진다. 즉, 아무 상관없는 것 같은 Repository 하나를 수정했는데, 엉뚱한 Controller Test가 실패하게 된다. Test 책임의 관점에서는 잘못된 것일 수 있다. 다만, 이런 것들이 내 로직 변경의 사이드이펙트로 나타나게 될 가능성이 큰 것이라, 큰 단점이라고 보기는 어렵다. (다분히 이론적인 단점이다)

2) 두번째 단점이 큰데, Controller Test를 만들기가 어렵다. 왜냐하면 DB 데이터(심지어 Relation이 있다면 그 Record까지)를 모두 Insert를 해두어야만 테스트가 가능해진다. 게다가 테스트가 서로 간섭하면 안되기 때문에 데이터도 깔끔하게 Test Method(혹은 Class)의 Scope에서만 데이터를 가지고 있어야 한다. 이것때문에 테스트 로직자체가 복잡해지고, 유지보수가 어렵게 되는 원인을 만들게 된다.

3) 두번째 단점의 연장선일 수 있는데, Integration Test라는 것은 좀더 세밀한 테스트를 어렵게 한다. 즉, 단위테스트라는 것은 mock을 사용해서 극한의 문제점들까지 테스트의 요소로 활용할 수 있다. 예를 들어, 어떤 Api를 호출했는데 Timeout이 발생한다거나, String을 파라미터로 받았는데 System Character(Beep)라던가 하는 식으로 테스트를 가능하게 한다. 그렇지만 Integration Test는 자주 일어나는 상황에 대해서만 테스트를 할 수 밖에 없다.

 

SpringBoot 컨텍스트를 활용하여 테스트 만들기를 선호하는 내 입장에서는,

코드 설계적인 부분을 이용해서 2), 3)번째 단점들을 보완한다.

즉, Controller의 영역은 철저하게 Request의 Converting, Validation

그리고 Response의 Converting에만 집중해서 로직을 만든다.

나머지는 모두 Service의 영역으로 넘겨서 로직을 조합한다.

예를 들어,

당장 생각이 잘 안나서 로직이 좀 간단하긴 하지만,

위처럼 컨트롤러에서 필터로직을 태우거나,

혹은 연관된 Bean들이 더 많아서 여러 가지 데이터를 한꺼번에 조합해서

Response를 돌려주는 경우가 생각보다 많다.

그런 경우, 3)번 단점이 크게 드러날 수 있다.

그래서 Controller를 조금 단순하게 만든다.

이렇게,

Controller는 파라미터의 NotEmptyCustomValidator.validate()만 Controller에서 하고

나머지는 모두 Service로 넘겨서,

Service 로직에서 Mock Test로 테스트를 풍성하게 한다. 이렇게 하면 3번의 단점을 보완할 수 있다.

 

2)번의 단점은, Controller Test를 컴팩트하게 만드는 것으로 트레이드오프한다.

즉, 테스트를 각 문제에 대해서 모든 것(케이스에 대한 100%)을 만든다기 보다는

대부분의 케이스에 대해서 만족(Request의 비율대비)하도록 만든다.

Success에 대한 요청과 빈번하게 발생하는 오류 케이스에 대해서만 만들어둔다.

한 달에 몇 번 발생하지 않는 케이스는 그냥 무시한다.

(대신 그걸로 장애가 발생하거나 하면 그 때 보강하는 식으로…)

 

마지막으로 3번째 방식으로 Sliced Controller Test를 하는 방식을 생각해보면,

1, 2번 방식의 장/단점을 반대로 가진다고 생각하면 된다.

Mock을 사용하기때문에 테스트가 더 상세하기 만들 수 있고,

무엇보다도 Controller에 한정된 부분으로만 Unit Test를 만들 수 있다.

(엄밀히 치면, Controller에 연결된 앞단 웹로직들은 타기 때문에 완전한 단위 테스트라고 보긴 어렵다)

그렇지만 그러다보면 Controller와 Service의 역할을 잘 분리하지 않고,

Controller의 로직이 비대해지는 문제점이 생길 수도 있다.

 

어느 쪽이든 테스트를 잘 만들어 둔다는 것은

내가 작성한 로직을 내 컨트롤 내에 두는 것과 같다고 생각한다.

 

“@SpringBootTest + mockMvc vs @WebMvcTest”의 4개의 댓글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다