마이크로서비스 테스트, Test Honeycomb 전략

recordsbeat·2023년 4월 8일
0
post-thumbnail

TL;DR

  • 테스트 피라미드는 어렵다
  • Test Honeycomb는 좋은 대안이 된다.
  • 의미 있는 테스트는 명확한 범위와 빠른 실행 그리고 신뢰성이 뒷받침되어야 한다.
  • 이를 충족하지 않는 이상 방법론은 무의미하다.

테스트는 중요하다. 하지만...

개발간 다들 테스트가 중요한 것은 알고있다.
그 중 테스트 피라미드는 바이블처럼 여겨진다. 나 또한 테스트 피라미드의 중요성을 항상 강조하며 이에 충실해야 한다고 생각했다.

ChatGPT 한테 물어본 테스트코드를 짜야하는 이유

테스트 코드를 작성하는 이유는 여러 가지가 있습니다.

1. 버그를 미리 찾아내기 위해
테스트 코드를 작성하면 개발자가 작성한 코드의 품질을 테스트할 수 있습니다. 테스트 코드를 실행하여 코드의 문제점이나 예상치 못한 결과가 발생하는 부분을 미리 찾아낼 수 있습니다. 이를 통해 버그를 최대한 빨리 찾아내고 수정할 수 있어 개발 생산성을 높일 수 있습니다.

2. 코드 변경에 대한 확신을 얻기 위해
테스트 코드가 있으면 코드 변경에 대한 확신을 얻을 수 있습니다. 코드를 변경하면 테스트 코드도 함께 실행하여 코드 변경에 대한 영향을 파악할 수 있습니다. 이를 통해 코드 변경에 대한 안정성을 높일 수 있습니다.

3. 코드 문서화를 위해
테스트 코드는 코드의 기능과 사용 방법을 문서화하는 역할도 합니다. 테스트 코드를 보면 코드의 기능과 사용 방법을 이해하기 쉽습니다. 따라서, 테스트 코드가 없으면 코드를 이해하기 어려울 수 있습니다.

그러나 현실적으로 테스트 피라미드를 모두 충족하기는 어려웠다.
각 테스트에 대한 best practice의 부재. 아티클을 읽어보아도 개념적인 이야기일뿐 실제 Production Code에 대입하는 것은 상당히 머리를 써야하는 일이었다.

테스트 피라미드가 어려운 이유

테스트 전략의 바이블, 테스트 피라미드를 가볍게 살펴보자

위로 갈수록 비용은 커지고, 아래로 갈수록 빠르며 간편하다

"testable한 코드를 작성하면 테스트 피라미드는 어렵지않게 충족할 수 있다!" 라고 떠들고 다니던 나였지만 이는 스스로 구렁텅이에 빠트리는 느낌이었다.

그 이유는

  1. testable의 기준이 모호하다. 간단하게는 런던파, 고전파와 같은 테스트 방법론에 따라 어떤 코드는 작은 단위로 쪼개지는 반면, 어떤 코드는 비교적 큰 기능단위로 테스트된다.

  2. 의존성을 배제할 수 없다. 인프라 환경(DB, Cache..) 구성 및 프레임워크 종속적인 로직(이를테면 Spring의 Transaction)들은 어쩔 수 없이 Production Code와 함께 동작해야한다.

  3. 리팩토링의 소요가 크다. 기존에 테스트 없거나 부족한 프로젝트에 테스트코드를 적용할 경우 리팩토링에 들여야하는 리소스가 적지 않다. 현업에서 충분치 않은 시간안에 많은 양의 리팩토링을 감내할 경우 극단적으로는 아키텍처를 다시 손봐야하는 경우도 생긴다.

테스트 피라미드 개념에 박혀, 테스트 코드를 작성이 단순 노가다 형태로 변질되기 시작했고 결국 피로감으로 인해 슬슬 테스트를 회피하기 시작했다.

그러다 언젠가 보았던 아티클이 생각났다.

Test Honeycomb

Testing of Microservices - Spotify

많은 서비스들이 그렇겠지만 나 또한 MSA 환경에서 개발을 하고 있다. 작은 단위의 코드 블럭부터 외부 연동이 들어간 큰 블럭까지 이를 모두 테스트한 다는 것은 분명 쉽지 않은 일이다. 또한 각 메소드의 특성마다 별도의 환경을 계속 구성해야함은 테스트코드 작성의 효율을 극단적으로 낮추기에 충분하다.

때문에 Spotify에서는 아래와 같은 형태의 Test Honeycomb를 제안했다.

  • Integrated Tests
    실운영과 비슷 혹은 동일하게 갖춰진 환경에서 진행하는 테스트.
    각 서비스가 다른 서비스와 환경을 공유하여 변경에 매우 취약하다.

  • Integration Tests
    서비스 자체를 격리된 환경으로 구성 후 Api endpoint를 호출하여 기능에 대한 종단 테스트를 진행한다. 기능에 대한 종단 테스트를 진행하기 때문에 많은 수의 테스트가 필요하지 않고, Production Code의 리팩토링 또한 테스트와 별개로 자유롭게 가능하다.

  • Implelmentation Detail Tests
    특이 상황에서 발생하거나 Integration로 도달하기 어려운 코드 블럭 내지 로직들을 테스트. 기존의 Unit Tests와 비슷한 개념으로 보인다.

다른 듯 하지만 같은 이야기

기존 테스트 피라미드에 익숙한 사람들과 Test Honeycomb를 주장하는 사람들 간의 갑을론박은 꽤나 잦은 일이었다. 이에 대해 네임드 Martin Fowler(마틴 파울러) 형님께서 한마디 거들었는데 요지는 "각 계층 대한 테스트 방법이 정의되지 않는 한 용어의 모호함은 계속된다." 와 같다.

마틴 파울러 - Sociable Test, Solitary Test

각자 주장하는 Unit test, Integration Test의 의미와 범위가 모두 다르기 때문에 근본적으로 무엇이 맞다라고 논쟁을 할 필요가 없다는 이야기지만, Test Honeycomb에 대한 반응은 아래와 같았다.

Sociable Tests는 실제 의존적으로 동작하는 대상과 함께 테스트를 하고
Solitary Tests는 테스트하고자 하는 객체를 고립시켜 진행한다.

"From this I infer that their definition of “unit test” is specifically what I would call a solitary unit test. Similarly their notion of integration test sounds very much like what I would call a sociable unit test. "
https://martinfowler.com/articles/2021-test-shapes.html

이 관점에서 마틴파울러가 바라보는 Test Honeycomb의 Integration Test는 Sociable Tests 라고 할 수 있다.

스포티파이 - Test Honeycomb

Spotify는 Microserivce 하나를 새로운 Unit으로 정의하고 Solitary Tests 관점에서 테스트환경을 격리 시켰다.

"you may have noticed that what we’ve been treating the Microservice as an isolated Component, tested through its contracts. In that sense the Microservice has become our new Unit"

https://engineering.atspotify.com/2018/01/testing-of-microservices/

애시당초 micro-service 의 의미자체가 경량화된 서비스를 뜻하므로 어찌보면 모놀리식의 Unit 과 닮았을 수 있다.

마틴파울러의 Component Test = Test Honeycomb의 Integration Test

https://martinfowler.com/articles/microservice-testing/#testing-component-in-process-diagram

그림을 살펴보면 Component Test Boundary도 결국 격리된 테스트 환경(in-memory db라던가..)을 구성하고 서버 애플리케이션의 전반적인 Layer를 모두 관통하도록 한다.

결론적으로 Test Honeycomb 에서 말하는 Integration Test는 마틴파울러의 Component Test와 매우 흡사하며, 이를 중점적으로 테스트할 것을 제안한다.

test honeycomb의 integration test 예제

실행과 장단점

상세한 묘사는 할 수 없지만 기본적으로 TC묘사를 위한 Test 클래스와 실제 given, when, then을 수행하는 Steps 클래스로 나누어 진행하였고, 테스트 환경 구성은 Test container 및 Mockito 라이브러리를 사용하여 공통적으로 구성하였다.

  1. 마이크로서비스의 인프라 및 외부서비스 의존성을 fixture로 정의한다.
  2. given 단계에서 각 fixture에 테스트 케이스에 해당하는 상태를 주입한다.
  3. when 단계에서 Presentation layer에 해당하는 부분(ex. API Endpoint, Message Listener)을 호출한다.
  4. then 단계에서 요청한 로직에 따라 검증을 진행한다.
@Test
void 추천_상품_목록과_상세정보를_정상_조회_한다.() {
  steps.given_추천_상품_데이터베이스();
  steps.given_상품_상세_정보_외부API_fixture();

  steps.when_추천_상품_목록_조회();

  steps.then_추천_상품_정상_조회_확인();
 }

장단점

장점

  • Production Code로부터 테스트가 자유롭다.
  • 테스트코드를 위한 리팩토링이 불필요하다.
  • 테스트 커버리지를 쉽게 충족할 수 있다.
    (Condition Coverage는 챌린지 구간이지만 Test Target 예외케이스 정의 및 Test Honeycomb의 Detail Implementation 으로 커버가능 하다)
  • given, then을 재사용할 수록 테스트 시나리오 구성이 쉽다.
  • 하나의 기능 즉, 스펙을 TC로 검증하기 때문에 Living Document와 같은 스펙문서의 역할을 한다.

단점

  • 테스트가 느리다.
    (사실 이 부분은 위의 테스트 피라미드 Component Test를 작성하게 되면 마찬가지인 상황이다.)

Test Honeycomb 전략은 테스트 피라미드와 견주어봤을 때 테스트 범위 정의, 테스트 계층별 환경 구성에 대한 소요가 많이 줄었고 무엇보다 중복된 테스트가 생산될 가능성이 매우 적어 이 또한 많은 비용을 줄일 수 있었다.

나름 테스트 피라미드를 충족하기 위해 계속해 시도를 해보았지만 현업에서 만족할만한 테스트 결과물을 찾기는 쉽지 않았다. 그러던 와중 Test Honeycomb는 어쩌면 가장 현실과 맞닿은 전략을 제시해주는 느낌이었고 실제로 만족스러운 결과를 보였다.

마치며

테스트는 반복적이고 지루하다. 때문에 테스트 코드 작성이 어려울수록 기피하기 마련이다. 하지만 테스트만큼 Living Document의 역할을 수행하며 스펙을 잘나타내는 방법도 없다고 본다.


사람들은 어떤 종류의 테스트를 얼마나 써야하는지에 대해 토론하는걸 좋아하는데,
정작 명확한 범위를 정의하고 빠르고 신뢰할 수 있으며, 합당한 이유에 의해 실패하는 의미있는 테스트를 짜는 팀은 거의 없다. 후자에 집중해라

마틴파울러의 글 On the Diverse And Fantastical Shapes of Testing 말미에 등장한 내용이다.

위 이야기는 내가 테스트 피라미드에서 생각을 바꾼 가장 큰 이유다.
테스트 코드는 의도와 그 범위가 명확해야 비로소 그 의미를 갖는다.

profile
Beyond the same routine

1개의 댓글

comment-user-thumbnail
2023년 4월 8일

테스트 코드를 작성하면서 '템플릿화 된 Component Test를 copilot 같은 AI도구가 endpoint마다 알아서 작성해주면 좋겠다' 라는 생각이 들었다. 머지 않은 미래에 사용가능하겠지..?

답글 달기