스터빙
고친 사람 github-actions[bot]
스터빙은 테스트에서 가짜 객체가 불렸을 때 내놓을 답을 미리 정해 두는 일입니다. 진짜 데이터베이스나 외부 서비스에 묻는 대신 정해 둔 답이 돌아옵니다. 그래서 테스트가 원하는 상황을 마음대로 만들 수 있습니다. 개발 중에 아직 안 만든 함수를 이름과 모양만 갖추고 몸통은 고정 값으로 채워 세워 두는 일도 같은 이름으로 부릅니다.
쉽고 빠른 이해
스터빙은 가짜 객체에게 「이렇게 물으면 이렇게 답해라」를 적어 주는 일입니다. 재고 조회가 불리면 늘 0 을 돌려주게 해 두면, 품절 상황을 데이터베이스 없이 시험할 수 있습니다.
이게 없으면 테스트가 진짜 데이터베이스와 외부 서비스에 매달립니다. 테스트가 느려집니다. 돌릴 때마다 결과가 바뀝니다. 품절이나 타임아웃 같은 상황은 일부러 만들기 어렵습니다.
- 진짜 대신 쓸 가짜 객체를 만듭니다
- 가짜가 무엇을 돌려줄지 정합니다. 값일 수도 있고 예외일 수도 있습니다
- 테스트할 코드를 돌리고 그 결과를 확인합니다
대가는 가짜가 진짜와 어긋날 수 있다는 것입니다. 정해 둔 답이 진짜의 동작과 다르면 테스트는 통과합니다. 그 코드가 운영에서 깨집니다.
그래서 스터빙은 데이터베이스나 외부 서비스처럼 느리거나 결과가 흔들리는 상대에만 씁니다. 메모리 안에서만 도는 단순한 객체는 진짜를 그대로 씁니다.
상세
연극 연습에서 상대 배우가 오지 않은 날을 떠올리면 됩니다. 스태프 한 명이 그 배우의 대사를 대본대로 읽어 줍니다. 주인공 배우는 상대가 없어도 자기 연기를 끝까지 맞춰 볼 수 있습니다.
스터빙은 그 대본을 적는 일입니다. 테스트 대상 코드가 다른 객체에게 무언가를 물을 때, 그 물음에 돌아갈 답을 미리 정해 둡니다. 테스트 대상 코드는 이번 테스트에서 시험하려는 코드입니다. 이 편은 주문 가능 여부를 판단하는 OrderService 하나를 예로 끝까지 씁니다.
협력 객체와 테스트 더블
OrderService 는 주문을 받을지 정하려고 재고를 조회합니다. 재고는 스스로 세지 않고 재고 저장소 객체에 묻습니다. 이렇게 테스트 대상 코드가 일을 하려고 부르는 다른 객체를 협력 객체라고 부릅니다.
협력 객체는 대개 바깥 세계와 맞닿아 있습니다. 재고 저장소는 데이터베이스를 읽습니다. 결제 클라이언트는 외부 결제 서비스를 부릅니다. 테스트에서 이런 것을 그대로 쓰면 테스트가 바깥 세계의 사정에 묶입니다.
그래서 테스트에서는 협력 객체를 가짜로 바꿔 끼웁니다. 진짜 대신 테스트에 끼우는 가짜 객체를 통틀어 테스트 더블이라고 부릅니다. 영화에서 배우 대신 위험한 장면을 찍는 대역에서 따온 이름입니다.
테스트 더블 가운데 정해 둔 답만 돌려주는 것을 스텁이라고 부릅니다. 스텁이 물건의 이름이라면 스터빙은 그 물건에 답을 적어 넣는 행위의 이름입니다.
스터빙이 필요한 까닭
진짜 협력 객체로 테스트하면 세 가지가 곤란합니다. 첫째는 속도입니다. 데이터베이스와 네트워크를 거치는 호출은 메모리 안의 호출보다 훨씬 느립니다.
둘째는 결과가 흔들린다는 것입니다. 재고는 다른 사람이 주문할 때마다 바뀝니다. 어제 통과한 테스트가 오늘은 재고가 달라서 실패합니다. 코드를 바꾸지 않았는데 결과가 바뀌는 테스트를 플래키 테스트라고 부릅니다.
셋째는 드문 상황을 만들기 어렵다는 것입니다. 「재고가 0 일 때 주문을 거절하나」를 보려면 데이터베이스의 재고를 일부러 0 으로 맞춰야 합니다. 「저장소가 타임아웃으로 실패하면 어떻게 되나」는 네트워크를 끊어야 볼 수 있습니다.
스터빙은 이 셋을 한꺼번에 풉니다. 정해 둔 답은 메모리 안에서 바로 돌아옵니다. 매번 같은 답이 돌아옵니다. 재고 0 이나 타임아웃도 한 줄로 적어 둘 수 있습니다.
스터빙이 도는 순서
스터빙을 쓰는 테스트는 늘 같은 순서를 밟습니다. 먼저 가짜를 만들고 답을 적습니다. 그 가짜를 테스트 대상 코드에 넘긴 뒤 코드를 돌립니다. 마지막으로 코드가 낸 결과를 확인합니다.
아래 그림은 OrderService 의 canOrder(item) 을 시험합니다. canOrder 는 상품 이름을 받아 주문할 수 있으면 true, 없으면 false 를 돌려줍니다. 그림은 「사과」를 주문할 수 있는지 묻는 경우입니다.
sequenceDiagram
participant 테스트
participant 대상 as OrderService
participant 스텁 as 재고 스텁
테스트->>스텁: 재고를 물으면 0 이라고 답해라
테스트->>대상: 스텁을 넘기고 canOrder 를 부른다
대상->>스텁: 사과 재고는?
스텁-->>대상: 0
대상-->>테스트: false
Note over 테스트: false 가 맞는지 확인한다
그림의 셋째 화살표가 핵심입니다. OrderService 는 자기가 진짜 저장소에 묻는지 가짜에 묻는지 모릅니다. 똑같이 묻고 돌아온 답으로 똑같이 판단합니다.
바꿔 끼울 틈
스텁을 넘기려면 테스트 대상 코드가 협력 객체를 스스로 만들지 않아야 합니다. OrderService 안에서 진짜 저장소를 new 로 만들면 테스트가 끼어들 틈이 없습니다.
그래서 협력 객체를 밖에서 넘겨받게 짭니다. 이 방식을 의존성 주입이라고 부릅니다. 흔히 생성자로 넘겨받습니다.
넘겨받는 타입은 인터페이스로 둡니다. 인터페이스는 메서드의 이름과 모양만 정해 두고 몸통은 비워 둔 틀입니다. 진짜 저장소와 스텁이 같은 인터페이스를 구현하면 OrderService 는 둘을 구별하지 않습니다.
interface StockRepository {
int count(String item);
}
class OrderService {
private final StockRepository stock;
OrderService(StockRepository stock) {
this.stock = stock;
}
boolean canOrder(String item) {
return stock.count(item) > 0;
}
}
canOrder 는 재고가 하나라도 있으면 true 를 돌려줍니다. 재고 수는 넘겨받은 stock 에게 묻습니다.
손으로 쓰는 스텁
스텁은 라이브러리 없이 손으로도 만듭니다. 자바에서는 람다로 한 줄에 씁니다. 람다는 메서드가 하나뿐인 인터페이스를 이름 없는 함수로 바로 구현하는 문법입니다.
StockRepository soldOut = item -> 0;
new OrderService(soldOut)
.canOrder("사과"); // false
StockRepository inStock = item -> 5;
new OrderService(inStock)
.canOrder("사과"); // true
soldOut 은 무엇을 물어도 0 을 답하는 스텁입니다. 그래서 canOrder 는 false 를 냅니다. inStock 은 늘 5 를 답하므로 결과가 true 로 바뀝니다. 데이터베이스는 한 번도 열리지 않았습니다.
정해 둘 수 있는 답
스터빙으로 정하는 답은 고정 값 하나에 그치지 않습니다. 테스트가 만들고 싶은 상황에 따라 네 가지를 씁니다.
| 답의 모양 | 만드는 상황 |
|---|---|
| 늘 같은 값 | 재고가 0 인 경우처럼 한 가지 상태를 고정한다 |
| 입력마다 다른 값 | 사과는 0, 배는 5 처럼 인자에 따라 답을 가른다 |
| 예외 | 저장소가 타임아웃으로 실패하는 경우를 만든다 |
| 부를 때마다 다른 값 | 첫 호출은 실패, 두 번째 호출은 성공처럼 재시도를 시험한다 |
셋째 줄이 특히 쓸모가 큽니다. 진짜 저장소로는 실패를 원하는 때에 일으키기 어렵습니다. 스텁은 예외를 던지게 적어 두기만 하면 됩니다.
StockRepository down = item -> {
throw new IllegalStateException("타임아웃");
};
지금의 canOrder 는 예외를 따로 다루지 않습니다. 그래서 이 스텁을 넘기면 예외가 그대로 호출한 쪽으로 올라갑니다. 나중에 예외를 잡아 주문을 거절하게 고친다면, 그 처리가 맞는지 이 스텁으로 시험할 수 있습니다.
모킹 프레임워크로 하는 스터빙
인터페이스의 메서드가 많아지면 손으로 쓰는 스텁이 길어집니다. 람다는 메서드가 하나일 때만 쓸 수 있습니다. 메서드가 열 개면 쓰지 않는 아홉 개까지 몸통을 채워야 합니다.
모킹 프레임워크는 이 일을 대신 해 줍니다. 인터페이스를 넘기면 가짜 객체를 만들어 줍니다. 그 가짜에 「이 메서드가 이 인자로 불리면 이 값을 돌려줘라」를 한 줄로 적게 해 줍니다.
손으로 쓸 때와 달리 쓰지 않는 메서드는 건드리지 않아도 됩니다. 자바에서는 Mockito가 이런 프레임워크로 널리 쓰입니다.
스터빙과 호출 검증
테스트 더블로 하는 일은 크게 둘입니다. 하나는 스터빙입니다. 다른 하나는 테스트 대상 코드가 협력 객체를 제대로 불렀는지 확인하는 호출 검증입니다.
두 일은 보는 방향이 반대입니다. 스터빙은 테스트 대상 코드로 들어가는 값을 정합니다. 호출 검증은 테스트 대상 코드에서 나가는 호출을 살핍니다.
| 스터빙 | 호출 검증 | |
|---|---|---|
| 다루는 것 | 협력 객체가 돌려주는 값 | 협력 객체가 받은 호출 |
| 방향 | 대상 코드로 들어온다 | 대상 코드에서 나간다 |
| 판정 근거 | 대상 코드가 낸 결과 | 호출이 있었나 · 인자가 맞나 |
| 알맞은 협력 | 재고 조회처럼 값을 묻는 메서드 | 메일 발송처럼 일을 시키는 메서드 |
테스트가 무엇을 보고 통과를 판정하는지에 따라 방식에 이름이 붙습니다. 대상 코드가 낸 결과 값을 보고 판정하는 방식을 상태 검증이라고 부릅니다. 재고 스텁을 넘기고 canOrder 가 false 를 냈는지 보는 테스트가 그렇습니다.
호출 검증으로 판정하는 방식은 행위 검증이라고 부릅니다. 주문이 끝난 뒤 메일 발송 메서드가 한 번 불렸는지 보는 테스트가 그렇습니다. 스터빙은 주로 상태 검증과 짝을 이룹니다.
불린 호출을 기록해 두고 나중에 확인하게 해 주는 테스트 더블을 목이라고 부릅니다. 목에도 답을 정해 둘 수 있습니다. 목 위에서 반환값을 정하는 것도 스터빙입니다. 스터빙은 물건의 종류가 아니라 행위의 이름이기 때문입니다.
스터빙의 대가
첫째 대가는 가짜가 진짜와 어긋날 수 있다는 것입니다. 진짜 저장소는 없는 상품을 물으면 예외를 던진다고 해 봅시다. 스텁은 같은 물음에 0 을 답하게 적혀 있습니다. 테스트는 통과합니다. 운영에서는 예외가 터집니다.
스텁의 답은 테스트를 쓴 사람이 진짜의 동작이라고 믿는 것입니다. 그 믿음이 틀리면 테스트는 틀린 믿음을 확인할 뿐입니다. 그래서 스터빙으로 짠 단위 테스트 위에 진짜 협력 객체를 붙여 돌리는 통합 테스트를 따로 둡니다.
둘째 대가는 테스트가 구현에 묶인다는 것입니다. 스터빙을 하려면 테스트 대상 코드가 어느 메서드를 어떤 인자로 부르는지 알아야 합니다. 결과가 같아도 부르는 메서드가 바뀌면 스텁을 고쳐야 합니다.
셋째 대가는 과한 스터빙입니다. 한 테스트에 스텁이 여럿 쌓이면 무엇을 시험하는지 읽기 어려워집니다. 스텁 준비가 테스트 본문보다 길어지면 테스트 대상 코드가 너무 많은 것에 기대고 있다는 신호로 읽기도 합니다.
쓰는 때와 안 쓰는 때
스터빙은 협력 객체가 바깥 세계와 맞닿아 있을 때 씁니다. 데이터베이스, 외부 서비스, 파일, 현재 시각이 그렇습니다. 현재 시각을 스터빙하면 「자정을 넘는 순간」 같은 경우를 기다리지 않고 시험할 수 있습니다.
드문 상황을 만들어야 할 때도 씁니다. 타임아웃, 재시도, 빈 결과처럼 진짜로는 일으키기 어려운 경우가 그렇습니다.
협력 객체가 메모리 안에서만 도는 단순한 객체면 스터빙하지 않습니다. 금액 계산 객체나 날짜 변환 함수는 진짜를 그대로 써도 빠릅니다. 결과도 늘 같습니다. 이런 것을 가짜로 바꾸면 대가만 치르고 얻는 것이 없습니다.
테스트 밖에서 쓰는 스터빙
스터빙이라는 말은 개발 중에도 쓰입니다. 아직 안 만든 함수를 이름과 모양만 갖춰 세워 두는 일입니다. 몸통은 고정 값을 돌려주거나 「아직 구현 안 됨」 예외를 던지게 둡니다.
이렇게 해 두면 그 함수를 부르는 쪽 코드를 먼저 짜고 컴파일할 수 있습니다. 영어로는 흔히 「stub out 한다」고 말합니다. 정해 둔 답을 돌려주는 가짜라는 점에서 테스트의 스텁과 뿌리가 같습니다.
관련 항목
스터빙으로 만드는 테스트 더블
테스트 더블 · 스텁 · 목 객체 · 페이크 · 스파이 · 더미
스터빙을 대신 해 주는 도구
모킹 프레임워크 · Mockito · 몽키 패칭 · 동적 프록시
스터빙이 쓰이는 테스트 종류
단위 테스트 · 통합 테스트 · 계약 테스트 · 테스트 픽스처 · QA와 테스트
스터빙과 맞세워지는 검증 방식
호출 검증 · 상태 검증 · 행위 검증 · 명령-조회 분리
스터빙이 기대는 설계 수단
의존성 주입 · 인터페이스 · 람다 표현식 · 생성자 주입 · 의존성 역전 원칙
스터빙으로 바꿔 끼우는 협력 대상
데이터베이스 · 외부 API · 타임아웃 · 예외 · 시계 추상화
스터빙이 막거나 부르는 테스트 문제
불안정한 테스트 · 깨지기 쉬운 테스트 · 과도한 모킹 · 테스트 격리
다른 이름: stubbing · 스텁 설정 · 스텁하기