9장 마이크로서비스 테스트 1부
9.1 마이크로서비스 아키텍처 테스트 전략
9.1.1 테스트 개요
- 테스트의 목적은 SUT(System Under Test)의 동작을 확인하는 것
- 여기서 시스템이란 클래스 수준일 수도, 전체 애플리케이션만큼 큰 단위일 수도 있다.
자동화 테스트 작성
- 자동화 테스트는 대부분 Junit과 같은 테스트 프레임워크로 작성한다.
- 자동화 테스트 단계
- 설정: SUT와 테스트 픽스처를 초기화
- 실행: SUT를 호출
- 확인: 호출 결과 및 SUT 상태를 단언
- 정리: 필요 시 픽스처를 정리
목/스텁을 이용한 테스트
- SUT는 대부분 디펜던시를 갖고 있고 이로 인해 테스트가 복잡하고 느려질 수 있다.
- 해결 방법으로 디펜던시를 테스트 더블로 대체하는 방법이 있다.
- 테스트 더블에네 스텁(stub)과 목(mock)이 존재한다.
- 스텁 - SUT에 값을 반환하는 테스트 더블
- 목 - SUT가 정확하게 디펜던시를 호출했는지 확인하는 테스트 더블 (목은 스텁의 일종)
테스트 종류
- 단위 테스트: 서비스의 작은 부분을 테스트
- 통합 테스트: 애플리케이션 서비스가 인프라 서비스 및 타 애플리케이션 서비스와 잘 연동되어 작동하는지 확인
- 컴포넌트 테스트: 개별 서비스에 대한 인수 테스트
- 종단 간 테스트: 전체 애플리케이션에 대한 인수 테스트
테스트 사분면: 테스트 분류 기준
테스트 사분면은 다음 두 가지 척도로 테스트를 분류하는 방법이다.
- 비즈니스에 관한 테스트인가, 기술에 관한 테스트인가
- 비즈니스 관련 테스트는 도메인 전문가의 용어로 기술
- 기술에 관한 테스트는 개발자와 구현의 용어를 써서 기술
- 테스트 목적이 프로그래밍 지원을 위함인가, 애플리케이션을 평가하기 위함인가
- 프로그래밍 지원 테스트는 일상 업무의 일부
- 애플리케이션 평가 테스트는 개선 부분을 식별하는 것이 목표
- 테스트 사분면으로 분류하면 각 영역별 4가지 테스트 유형이 도출된다.
- 프로그래밍/기술 관련 지원 - 단위/통합 테스트
- 프로그래밍/비즈니스 롼련 지원 - 컴포넌트/종단 간 테스트
- 애플리케이션/비즈니스 관련 평가 - 사용성/예비 테스트
- 애플리케이션/기술 관련 평가 - 비기능(성능 등) 인수 테스트
테스트 피라미드: 테스트 역량을 집중
- 애플리케이션 동작을 확신하려면 여러 종류의 테스트를 작성해야 한다.
- 하지만 테스트 범위가 늘어나면 실행 시간, 복잡도가 증가하고 그만큼 신뢰성은 떨어지기 마련
- 특히 거시적인 종단 간 테스트는 작성하기 어렵고 느리고 복잡해 미덥지 못할 때가 많다.
- 테스트 피라미드
- 피라미드 하부는 빠르고 간단하고 미더운 테스트가 존재
- 피라미드 상부는 느리고 복잡하고 취약한 종단 간 테스트가 위치
- 테스트 피라미드에서 핵심은 상부로 올라갈수록 작성하는 테스트 개수가 줄어든다는 것
- 즉 단위 테스트는 많이, 종단 간 테스트는 적게 작성해야 한다.
9.1.2 마이크로서비스 테스트
- IPC는 모놀리식보다 MSA에서 더 큰 비중을 차지한다.
- MSA는 팀별로 각자 서비스를 개발하고 꾸준히 API를 발전시키는 분산 시스템이다.
- 서비스는 다양한 상호 작용 스타일과 IPC로 서로 통신한다.
- REST, gRPC, 동기 또는 비동기 프로토콜
- 두 서비스 간 상호 작용은 두 서비스 간의 합의 또는 계약이다.
- 개발자는 자신이 소비하는 서비스 API가 안정적인지 미리 확인해야 한다.
- 개발자는 자신이 제공하는 API를 함부로 바꾸지 않도록 주의해야 한다.
- 두 서비스가 상호 작용 가능한지는 두 서비스를 모두 실행하고 API를 호출한 후 검증하면 확인할 수 있다.
- 단 이러한 종단 간 테스트는 무수히 많은 디펜던시를 모두 실행시켜야 하는 난관이 존재
컨슈머 주도 계약 테스트
- API 게이트웨이의
OrderServiceProxy
는 /orders/{orderId} 같은 REST 끝점을 여럿 호출한다.
- 두 서비스는 컨슈머-프로바이더 관계
- 컨슈머는 API 게이트웨이, 프로바이더는 주문 서비스
- 컨슈머 계약 테스트
- 프로바이더의 API가 컨슈머가 기대하는 바와 일치하는지 확인하는 통합 테스트
- 초점은 프로바이더 API의 ‘형상’이 컨슈머가 기대한 것과 부합하는지 확인하는 것
- REST 끝점의 경우 컨슈머 계약 테스트에서 프로바이더에 대해 확인하는 부분
- 기대한 HTTP 메서드와 경로인가?
- 기대한 헤더를 받는가?
- 요청 본문을 받는가?
- 기대한 상태 코드, 헤더, 본문이 포함된 응답을 반환하는가?
- 컨슈머 계약 테스트는 프로바이더의 비즈니스 로직을 빠짐없이 체크하는 테스트가 아니다.
- 컨슈머 개발 팀은 계약 테스트 스위트 작성 후 프로바이더의 테스트 스위트에 추가한다.
- 이를테면 git pull request 방식
- 각 테스트 스위트는 각 컨슈머에 해당하는 주문 서비스의 API를 테스트한다.
- 이렇게 취합된 테스트 스위트는 주문 서비스 배포 파이프라인으로 실행한다.
서비스 테스트: 스프링 클라우드 컨트랙트
- 스프링 클라우드 컨트랙트
- 스프링용 컨슈머 계약 테스트 프레임워크
- 계약을 그루비 DSL로 작성할 수 있다.
- 프로바이더의 계약 테스트 코드를 생성하고 컨슈머 통합 테스트용 목을 구성
- 예) API 게이트웨이 개발자가 주문 서비스의 컨슈머 계약 테스트를 작성
- API 게이트웨이 팀이 하나 이상의 계약 작성
- 각 계약은 기대되는 HTTP 요청과 응답으로 깃 풀 리퀘스트로 주문 서비스 팀에 전달
- 주문 서비스 팀은 컨슈머 계약 테스트로 주문 서비스를 테스트
- 테스트 코드는 스프링 클라우드 컨트랙트에서 자동 생성
- 주문 서비스 팀은 테스트한 계약을 메이븐 저장소로 발행
- API 게이트웨이 팀은 주문 서비스 팀이 발행한 계약으로 API 게이트웨이 테스트를 작성
- 아래 코드는 HTTP 요청/응답으로 구성된 스프링 클라우드 컨트랙트 예제
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
url '/orders/1223232'
}
response {
status 200
headers {
header('Content-type': 'application/json;charset=UTF-8')
}
body("{...}")
}
}
- 계약별 요청/응답은 테스트 데이터와 기대되는 동작의 명세라는 이중 역할을 수행
- 컨슈머 쪽 테스트에서 계약은 목과 유사한 스텁을 구성해 주문 서비스 동작을 시뮬레이션하는 용도로 사용
컨슈머 계약 테스트: 메시징 API
- 도메인 이벤트를 구독하고 비동기 요청/응답 통신을 하는 서비스 역시 컨슈머다.
- 스프링 클라우드 컨트랙트로 메시징 기반 상호 작용도 테스트할 수 있다.
- 프로바이더가 이벤트를 발생시키도록 만들고 계약의 이벤트와 일치하는지 확인
- 컨슈머 테스트는 이 이벤트를 처리할 수 있는지 확인
9.1.3 배포 파이프라인
- 배포 파이프라인은 테스트 스위트 실행 단계, 서비스 릴리스/배포 단계 순서로 구성
- 완전 자동화가 이상적이지만 일부는 수작업일 수 있다.
- 코드가 파이프라인을 흘러갈수록 테스트 스위트는 점점 프로덕션과 유사하며 엄격한 테스트 환경에 종속된다.
- 배포 파이프 라인 예시
- 사전-커밋 테스트 단계: 개발자가 커밋하기 전에 단위 테스트 실행
- 커밋 테스트 단계: 서비스 컴파일 후 단위 테스트를 실행 후 정적 코드 분석 수행
- 통합 테스트 단계: 통합 테스트 실행
- 컴포넌트 테스트 단계: 서비스 컴포넌트 테스트를 실행
- 배포 단계: 프로덕션에 서비스를 배포
9.2 서비스 단위 테스트 작성
- 종단 간 테스트는 복잡하고 속도가 느리기에 단위 테스트 작성이 필요하다.
- 일반적으로 단위 테스트는 테스트 피라미드 최하부에 있으며 해당 클래스가 잘 동작하는지 확인하는 것이 목표
- 단위 테스트 종류
- 독립 단위 테스트: 클래스 디펜던시를 목 객체로 나타내고 클래스를 따로 테스트
- 공동 단위 테스트: 클래스와 디펜던시를 테스트
- 어떤 단위 테스트를 할지는 클래스의 책임과 아키텍처 역할마다 다르다.
- 전형적인 육각형 아키텍처에슨 아래와 같이 사용하는 경우가 많다.
- 컨트롤러, 서비스, 어댑터 클래스는 독립 단위 테스트
- 엔티티와 벨류 긱체는 공동 단위 테스트
- 클래스별 많이 쓰는 테스트 전략
- 공동 단위 테스트
- 영속적으로 식별 가능한 엔티티
- 밸류 객체
- 여러 서비스에 걸쳐 데이터 일관성을 유지하는 사가
- 독립 단위 테스트
- 비즈니스 로직을 구현한 서비스 클래스
- HTTP 요청을 처리하는 컨트롤러
- 인바운드/아웃바운드 메시징 게이트웨이
9.2.1 단위 테스트 작성: 엔티티
- 단위 테스트는 비즈니스 로직을 빈틈없이 테스트
- 실행이 매우 빨라 컴파일 타임 테스트로 사용 가능
- 공동 단위 테스트로 작성하기에 디펜던시도 꼭 함께 테스트해야 한다.
9.2.2 단위 테스트 작성: 벨류 객체
- 벨류 객체는 불변이고 부수 효과를 걱정할 필요가 없어 테스트하기 쉽다.
9.2.3 단위 테스트 작성: 사가
- 중요 비즈니스 로직이 구현된 사가 클래스는 반드시 테스트를 해야 한다.
- 사가는 사가 참여자에게 커맨드 메시지를 보내고 응답을 처리하는 영속적 객체
- 정상적인 시나리오와 실패해서 롤백되는 다양한 시나리오를 테스트해야 한다.
- 실제 DB와 메시지 브로커를 스텁과 함께 사용해서 다양한 사가 참여자를 시뮬레이션하는 테스트를 작성한다.
- 실제 커맨드 채널을 구독하고 테스트하면 느리기 때문에 모킹 테스트를 작성하는 것이 낫다.
9.2.4 단위 테스트 작성 도메인 서비스
OrderService
같은 도메인 서비스는 엔티티와 리포지터리를 호출하며 도메인 이벤트를 발행한다.
- 도메인 서비스는 리포지터리 및 메시징 클래스 같은 디펜던시를 모킹한 독립 단위 테스트를 수행하는 편이다.
- 서비스 테스트 순서
- 설정: 서비스 디펜던시의 목 객체 구성
- 실행: 서비스 메서드 호출
- 확인: 서비스 메서드가 올바른 값을 반환하고 디펜던시가 올바르게 호출되었는지 확인
9.2.5 단위 테스트 작성 컨트롤러
- 주문 서비스 같은 서비스는 HTTP 요청을 처리하는 컨트롤러를 하나 이상 가진다.
- 서비스 테스트처럼 디펜던시를 모킹하여 독립 단위 테스트로 구성하는 방법이 좋다.
9.2.6 단위 테스트 작성: 이벤트/메시지 핸들러
- 서비스는 흔히 외부 시스템에서 전송된 메시지를 처리한다.
- ex)
OrderEventConsumer
라는 메시지 어댑터
- 메시지 어댑터는 컨트롤러처럼 도메인 서비스를 호출하는 단순 클래스