통합 테스트
고친 사람 github-actions[bot]
통합 테스트는 따로 만든 부분들을 이어 붙인 뒤 함께 제대로 도는지 확인합니다. 내 코드가 진짜 데이터베이스와 말이 통하는지, 서비스 둘이 서로의 요청을 알아듣는지를 봅니다. 부분마다 검사를 마쳐도 붙인 곳에서 깨지는 일이 흔합니다. 그 붙인 곳을 겨냥한 검사입니다.
쉽고 빠른 이해
무슨 일을 하나 — 여러 부분을 진짜로 이어 놓고 한 흐름을 끝까지 불러 봅니다. 주문을 저장하는 코드를 진짜 데이터베이스에 붙여 주문 하나를 저장하고 다시 읽어 보는 식입니다.
왜 하나 — 부분 하나만 검사할 때는 이웃을 가짜로 바꿔 끼웁니다. 가짜는 진짜와 다르게 굴 수 있어서 컬럼 이름 오타나 틀린 쿼리가 숨습니다. 붙여서 돌려 봐야 이런 어긋남이 드러납니다.
어떻게 도나
- 테스트용 데이터베이스처럼 진짜 이웃을 띄웁니다
- 내 코드를 그 이웃에 이어 한 흐름을 끝까지 부릅니다
- 돌아온 값과 이웃에 남은 흔적을 확인하고 뒷정리합니다
대가 — 이웃을 띄우느라 느리고 준비가 무겁습니다. 깨졌을 때 어느 연결이 원인인지 한 번 더 찾아야 합니다.
언제 쓰나 — 데이터베이스나 다른 서비스와 맞닿는 코드에 씁니다. 경우가 많은 계산은 단위 테스트로 넘깁니다.
상세
이 절은 주문 서비스 하나를 가지고 통합 테스트를 봅니다. 주문 서비스는 주문을 데이터베이스에 저장합니다. 주문을 받으면 우리 회사가 따로 띄운 재고 서비스에 물건이 남았는지 먼저 묻습니다.
결제는 바깥 회사가 운영하는 결제 서버에 맡깁니다. 그 서버에 요청을 보내는 일은 주문 서비스 안의 결제 연결 모듈이 합니다.
새 집에 전기 공사를 한다고 해 봅시다. 전구도 스위치도 가게에서 하나씩 검사를 마친 물건입니다. 그래도 벽 속 전선으로 둘을 이은 뒤 스위치를 눌러 봐야 불이 켜지는지 압니다. 전선이 엉뚱한 스위치에 물려 있으면 멀쩡한 부품 둘로도 불이 안 켜집니다.
통합 테스트는 프로그램의 부분 둘 이상을 진짜로 이어 붙여 함께 검사합니다. 부분은 내가 쓴 모듈일 수 있습니다. 모듈은 한 가지 일을 맡아 따로 만든 코드 묶음입니다. 데이터베이스처럼 프로그램 바깥에서 도는 것도 부분이 됩니다.
검사가 겨냥하는 곳은 부분 자체가 아니라 부분과 부분이 맞닿는 이음매입니다. 주문 서비스가 데이터베이스에 쿼리를 보내는 곳이 그런 이음매입니다. 이음매가 따로 검사를 받아야 하는 까닭은 단위 테스트가 보는 범위에 있습니다.
단위 테스트는 함수나 클래스 하나를 떼어 검사합니다. 그 이웃은 테스트 더블이라 부르는 가짜로 바꿔 끼웁니다. 가짜는 테스트를 쓴 사람이 짐작한 대로만 움직입니다. 진짜 이웃이 짐작과 다르게 굴면 단위 테스트는 그 차이를 못 봅니다.
이음매에서 나는 결함
붙였을 때만 드러나는 결함은 몇 갈래로 모입니다. 아래 표는 백엔드에서 흔히 만나는 넷입니다.
| 결함 | 예 | 단위 테스트에서 숨는 까닭 |
|---|---|---|
| 쿼리가 틀림 | 컬럼 이름 오타, 데이터베이스가 모르는 문법 | 가짜 저장소는 쿼리를 읽지 않는다 |
| 주고받는 모양이 어긋남 | 보내는 쪽은 userId, 받는 쪽은 user_id 를 기다림 |
가짜는 내가 짐작한 모양으로만 답한다 |
| 값의 단위가 어긋남 | 한쪽은 초로, 다른 쪽은 밀리초로 읽음 | 양쪽 테스트가 각자 자기 단위로 통과한다 |
| 설정이 틀림 | 접속 주소, 계정, 테이블 권한 | 가짜는 어디에도 접속하지 않는다 |
네 결함은 부분 하나만 봐서는 틀린 데가 없다는 점이 같습니다. 보내는 코드도 받는 코드도 제 나름으로는 맞습니다. 둘을 이어 놓고 한 번 불러 봐야 어긋남이 드러납니다.
통합 테스트가 가리키는 범위
통합 테스트라는 이름은 팀마다 가리키는 범위가 다릅니다. 크게 좁은 쪽과 넓은 쪽 두 갈래로 쓰입니다. 아래 그림은 같은 주문 시스템을 두 갈래로 검사할 때 무엇을 진짜로 붙이는지를 보입니다.
flowchart TD
subgraph 좁은["좁은 통합 테스트"]
A["주문 저장소"] --> B[("진짜 데이터베이스")]
end
subgraph 넓은["넓은 통합 테스트"]
D["주문 서비스"] --> E[("진짜 데이터베이스")]
D --> F["진짜 재고 서비스"]
end
좁은 쪽은 내 코드 한 덩어리를 바깥 것 하나에 붙입니다. 주문 저장소 클래스를 진짜 데이터베이스에 붙여 보는 것이 전형입니다. 나머지 이웃은 가짜로 둡니다. 금방 끝나고 깨졌을 때 원인을 찾기도 쉽습니다.
넓은 쪽은 주문 서비스와 재고 서비스를 함께 띄워 놓고 요청 하나가 끝까지 지나가는지 봅니다. 서비스를 잘게 나눈 마이크로서비스 시스템에서 이 뜻으로 많이 씁니다. 붙인 것이 많은 만큼 느립니다. 깨지면 원인이 어느 서비스에 있는지부터 찾아야 합니다.
이 넓은 쪽은 종단 간 테스트와 경계가 흐립니다. 종단 간 테스트는 사용자가 쓰는 흐름 전체를 처음부터 끝까지 돌려 보는 테스트입니다. 같은 「통합 테스트」라는 말이 몇 초짜리 검사와 몇십 분짜리 검사를 함께 가리키기도 하는 까닭입니다.
두 갈래가 같이 지키는 것은 하나입니다. 이음매 한쪽을 가짜 대신 진짜로 붙인다는 점입니다.
모듈을 붙이는 순서
모듈이 많은 프로그램은 모듈을 한꺼번에 붙일지, 몇 개씩 차례로 붙일지를 골라야 합니다. 이 소절은 모듈 넷이 층을 이루는 아래 그림 하나를 두고 세 방식을 봅니다.
flowchart TD
A["요청 받는 모듈"] --> B["주문 서비스"]
B --> C["주문 저장소"]
B --> D["결제 연결 모듈"]
가장 단순한 방식은 네 모듈을 다 만든 뒤 한꺼번에 붙이는 것입니다. 이것을 빅뱅 통합이라고 부릅니다. 준비는 가장 적습니다. 대신 깨지면 이음매 셋 중 어디가 원인인지 가리기 어렵습니다.
그 반대는 모듈을 하나씩 붙이고 붙일 때마다 검사하는 점진적 통합입니다. 방금 붙인 이음매가 원인일 때가 많아서 원인을 좁히기 쉽습니다. 점진적 통합은 어느 끝에서 시작하느냐로 다시 둘로 나뉩니다.
위에서부터 붙이는 하향식 통합은 요청 받는 모듈과 주문 서비스를 먼저 잇습니다. 이때 주문 저장소와 결제 연결 모듈은 아직 없습니다. 그 둘 대신 스텁을 끼웁니다.
스텁은 부르면 정해 둔 값을 돌려주기만 하는 가짜입니다. 주문 서비스가 결제 연결 모듈을 부르면, 그 대신 선 스텁이 늘 「결제 성공」만 돌려주는 식입니다.
아래에서부터 붙이는 상향식 통합은 주문 저장소부터 데이터베이스에 붙여 봅니다. 이번에는 저장소를 불러 줄 위 모듈이 아직 없습니다. 저장소를 대신 불러 주는 작은 코드를 씁니다.
이 코드를 테스트 드라이버라고 부릅니다. 위 모듈 대신 저장소를 불러 주문을 넣고 꺼내 봅니다.
| 방식 | 먼저 붙이는 모듈 | 없는 모듈 대신 쓰는 것 | 깨졌을 때 |
|---|---|---|---|
| 빅뱅 통합 | 전부 한꺼번에 | 없다 | 이음매 여럿을 다 의심한다 |
| 하향식 통합 | 위층 | 스텁 | 방금 붙인 이음매부터 본다 |
| 상향식 통합 | 아래층 | 테스트 드라이버 | 방금 붙인 이음매부터 본다 |
표에서 보듯 점진적 두 방식은 가짜를 끼우는 방향만 반대입니다. 위아래를 함께 붙여 가며 가운데에서 만나는 방식도 있습니다. 이 방식을 샌드위치 통합이라고 부릅니다.
테스트 하나의 모양
이 소절은 좁은 통합 테스트 하나를 봅니다. 주문 저장소를 진짜 데이터베이스에 붙여 주문을 저장하고 다시 읽습니다.
먼저 테스트용 데이터베이스가 있어야 합니다. 테스트를 시작할 때 컨테이너로 데이터베이스를 하나 띄우고 끝나면 지우는 방식을 흔히 씁니다. 컨테이너는 프로그램을 필요한 파일과 함께 묶어 다른 프로그램과 떨어진 채로 띄우는 실행 단위입니다. 운영과 같은 종류의 데이터베이스를 띄우므로 쿼리가 운영에서와 같게 해석됩니다.
아래는 자바 테스트 도구인 JUnit 으로 쓴 테스트입니다. testDb 는 방금 띄운 테스트용 데이터베이스에 연결된 객체입니다.
@Test
void 저장한_주문을_다시_읽는다() {
// 준비
var repo = new OrderRepository(testDb);
// 실행
repo.save(new Order("A-1", 18000));
var found = repo.findById("A-1");
int price = found.price(); // 18000
// 확인
assertEquals(18000, price);
}
모양은 단위 테스트와 같습니다. 준비하고, 부르고, 나온 값을 확인합니다. 다른 점은 testDb 가 가짜가 아니라 진짜 데이터베이스라는 것입니다.
그래서 save 가 만든 INSERT 문과 findById 가 만든 SELECT 문이 데이터베이스에서 진짜로 해석됩니다. 저장소 코드 안의 컬럼 이름이 하나라도 틀리면 이 테스트는 save 줄에서 깨집니다. 앞의 표에서 본 「쿼리가 틀림」을 이 테스트가 잡습니다.
테스트 사이의 뒷정리
통합 테스트는 진짜 데이터베이스에 흔적을 남깁니다. 앞 테스트가 저장한 주문 A-1 이 남아 있으면, 같은 번호로 저장하는 다음 테스트가 중복 키 오류로 깨집니다. 테스트를 도는 순서에 따라 결과가 달라지는 것입니다.
흔한 해결은 테스트마다 트랜잭션을 열고 끝에서 되돌리는 것입니다. 트랜잭션은 여러 변경을 한 묶음으로 반영하거나 한 번에 취소하는 단위입니다. 끝에서 취소하면 그 테스트가 쓴 행이 전부 사라집니다.
트랜잭션으로 못 덮는 경우도 있습니다. 검사할 코드가 스스로 트랜잭션을 확정해 버리면 테스트가 되돌릴 수 없습니다. 이때는 테스트를 시작하기 전에 관련 테이블을 비웁니다.
돌릴 때마다 결과가 달라지는 테스트를 불안정한 테스트라고 부릅니다. 남은 데이터, 네트워크 지연, 현재 시각처럼 흔들리는 것에 기대면 이렇게 됩니다. 통합 테스트는 이런 것을 단위 테스트보다 많이 품고 있어서 불안정해지기 쉽습니다.
바깥 회사의 서비스를 붙일 때
결제 서버처럼 다른 회사가 운영하는 서비스는 테스트마다 진짜로 부르기 어렵습니다. 돈이 오가기도 하고 호출 횟수가 막혀 있기도 합니다. 그래서 그 서비스처럼 답하는 흉내 서버를 띄웁니다.
흉내 서버를 쓰면 내 코드는 요청을 네트워크로 진짜 보냅니다. 요청을 만드는 코드와 응답을 읽는 코드가 진짜로 검사됩니다. 가짜 객체를 끼울 때보다 진짜 쪽으로 한 걸음 더 간 셈입니다.
흉내 서버는 상대 회사가 응답 모양을 바꿔도 모릅니다. 이 틈을 메우려고 계약 테스트를 함께 쓰기도 합니다. 계약 테스트는 양쪽이 주고받을 모양을 문서로 합의해 둡니다. 그리고 각자 그 합의를 지키는지 따로 검사합니다.
다른 테스트와 나뉘는 선
테스트는 한 번에 얼마나 넓게 보느냐로 나뉩니다. 통합 테스트는 가운데에 섭니다.
| 종류 | 진짜로 붙이는 것 | 걸리는 시간 | 깨졌을 때 |
|---|---|---|---|
| 단위 테스트 | 없다. 이웃은 전부 가짜 | 가장 짧다 | 깨진 조각이 바로 나온다 |
| 통합 테스트 | 부분 둘 이상, 또는 부분과 데이터베이스 같은 바깥 | 중간 | 어느 이음매인지 찾아야 한다 |
| 종단 간 테스트 | 사용자가 쓰는 흐름 전체 | 가장 길다 | 원인이 어디든 있을 수 있다 |
흔한 권장은 좁은 테스트일수록 많이 두는 것입니다. 단위 테스트를 가장 많이, 통합 테스트를 그보다 적게, 종단 간 테스트를 가장 적게 둡니다. 개수를 층으로 쌓으면 아래가 넓은 모양이 되어 테스트 피라미드라고 부릅니다.
잘 맞는 코드와 덜 맞는 코드
통합 테스트가 가장 값을 하는 곳은 바깥과 맞닿는 코드입니다. 데이터베이스에 쿼리를 보내는 저장소, 다른 서비스를 부르는 클라이언트, 접속 설정이 그렇습니다. 이런 코드는 가짜로 검사하면 가짜가 돌려준 값을 다시 확인하는 셈이 됩니다.
조건에 따라 답이 갈리는 계산에는 덜 맞습니다. 할인 규칙에 경우가 스무 개면 통합 테스트도 스무 개가 필요합니다. 하나하나가 데이터베이스를 띄우느라 느립니다.
계산은 단위 테스트로 경우마다 검사합니다. 통합 테스트는 이음매마다 몇 개만 둡니다. 두 테스트가 맡는 일을 이렇게 나눕니다.
언제 돌리나
통합 테스트는 단위 테스트보다 오래 걸려서 코드를 고칠 때마다 전부 돌리기는 어렵습니다. 그래서 주로 코드를 합칠 때 돌립니다.
지속적 통합은 누가 코드를 올릴 때마다 서버가 빌드하고 테스트를 돌려 주는 방식입니다. 이 흐름에서는 흔히 단위 테스트를 먼저 돌립니다. 그게 통과하면 통합 테스트로 넘어갑니다. 금방 끝나는 검사로 먼저 걸러 내는 순서입니다.
관련 항목
통합 테스트와 범위로 나뉘는 테스트
단위 테스트 · 컴포넌트 테스트 · 계약 테스트 · 시스템 테스트 · 종단 간 테스트 · 인수 테스트 · 스모크 테스트 · 회귀 테스트
테스트를 범위와 크기로 가르는 분류
좁은 범위 테스트 · 중간 범위 테스트 · 넓은 범위 테스트 · 작은 테스트 · 중간 테스트 · 테스트 피라미드
통합 테스트가 모듈을 붙이는 방식
빅뱅 통합 · 점진적 통합 · 하향식 통합 · 상향식 통합 · 샌드위치 통합
붙이지 않은 이웃 대신 끼우는 가짜
테스트 더블 · 스텁 · 테스트 드라이버 · 목 객체 · 페이크 객체 · 목 서버
통합 테스트가 이웃을 띄울 때 쓰는 도구
컨테이너 · Docker · Docker Compose · Testcontainers · 인메모리 데이터베이스 · WireMock · JUnit
통합 테스트가 붙여 보는 바깥 시스템
데이터베이스 · 메시지 큐 · 캐시 · 외부 API · 파일 시스템 · 마이크로서비스
통합 테스트가 검사하는 이음매
인터페이스 · API · 트랜잭션 · 직렬화 · 스키마 · 데이터베이스 마이그레이션
통합 테스트에서 자주 나는 문제
불안정한 테스트 · 테스트 순서 의존성 · 테스트 데이터 오염 · 깨지기 쉬운 테스트
통합 테스트가 돌아가는 개발 단계
지속적 통합 · 지속적 전달 · 빌드 · 배포 파이프라인 · 스테이징 환경
통합 테스트와 함께 결함을 잡는 수단
정적 분석 · 동적 분석 · 카오스 엔지니어링 · 부하 테스트 · 코드 리뷰 · 퍼징
통합 테스트가 속하는 상위 분류
다른 이름: integration test · integration testing · 통합 시험 · 연동 테스트