페이크
고친 사람 github-actions[bot]
페이크는 테스트하는 동안 진짜 부품의 일을 대신 해냅니다. 정해 둔 답만 돌려주는 가짜와 달리 스스로 동작해서 답을 만듭니다. 데이터베이스 대신 메모리에 값을 담아 두는 저장소가 대표입니다. 운영에 필요한 일을 건너뛰는 대신 테스트는 금방 끝나고 매번 같은 결과를 냅니다.
쉽고 빠른 이해
무슨 일을 하나 — 테스트할 때 진짜 부품 대신 일을 해 주는 작은 구현입니다. 회원 가입 코드를 시험할 때 진짜 데이터베이스 대신, 이메일을 메모리에 담아 두는 저장소를 넘기는 식입니다.
왜 하나 — 진짜 데이터베이스를 붙이면 테스트가 느려집니다. 앞 테스트가 남긴 데이터에 결과가 흔들리기도 합니다. 답을 하나씩 정해 주는 가짜로는 「저장한 뒤 다시 찾는」 흐름을 흉내 내기가 번거롭습니다.
어떻게 도나
대가 — 페이크가 진짜와 다르게 굴면 테스트는 통과합니다. 그런데 운영에서는 깨집니다. 페이크도 코드라서 진짜가 바뀌면 함께 고쳐야 합니다.
상세
상세는 회원 가입 코드와 회원 저장소 하나로 끝까지 갑니다. 먼저 정해 둔 답을 돌려주는 가짜로는 무엇이 번거로운지 봅니다. 그다음 메모리에 담는 저장소를 페이크로 만들어 끼우는 과정을 코드로 따라갑니다.
답 하나로는 흉내 내기 번거로운 흐름
회원 가입 코드를 시험한다고 합시다. 이 코드는 이미 가입한 이메일이면 거절합니다. 처음 보는 이메일이면 저장합니다. 이렇게 시험하려는 코드를 테스트 대상이라고 부릅니다.
가입 코드는 일을 하려고 회원 저장소를 불러 씁니다. 회원 저장소는 회원을 데이터베이스에 저장하고 찾아 주는 부품입니다.
진짜 데이터베이스를 붙인 채로 시험하면 곤란한 일이 생깁니다. 데이터베이스 서버를 띄우고 테이블을 만들어야 테스트가 돕니다. 읽고 쓸 때마다 네트워크와 디스크를 거치므로 느립니다. 앞 테스트가 저장한 회원이 남아 있으면 다음 테스트의 결과가 바뀝니다.
그래서 테스트하는 동안만 진짜 대신 가짜를 끼웁니다. 이런 가짜를 통틀어 테스트 더블이라고 부릅니다.
가장 흔한 테스트 더블은 정해 둔 답만 돌려주는 스텁입니다. 「같은 이메일로 두 번 가입하면 두 번째는 거절된다」를 스텁으로 시험해 보면 번거로움이 드러납니다. 스텁은 「이 이메일이 있나」라는 물음에 첫 번째는 없다고, 두 번째는 있다고 답하게 순서대로 적어 둬야 합니다. 가입 코드가 저장소를 몇 번, 어떤 순서로 부르는지를 테스트가 미리 알아야 한다는 뜻입니다.
흐름이 길어질수록 적어 둘 답도 늘어납니다. 가입한 뒤 탈퇴하고 다시 가입하는 흐름이면 답 순서가 여섯, 일곱 줄이 됩니다. 가입 코드가 저장소를 한 번 더 부르게 고치기만 해도 순서가 어긋나 테스트가 깨집니다.
메모리에 담는 저장소
페이크는 답을 적어 두는 대신 스스로 동작합니다. 저장하라고 하면 저장합니다. 찾으라고 하면 저장해 둔 것에서 찾습니다. 담아 두는 곳이 데이터베이스가 아니라 메모리일 뿐입니다.
페이크를 끼우려면 먼저 저장소가 할 일을 인터페이스로 적어 둡니다. 인터페이스는 메서드 이름과 모양만 정하고 몸통은 비워 둔 약속입니다. 진짜 저장소와 페이크가 같은 약속을 저마다 구현할 수 있게 됩니다.
다음으로 가입 코드가 저장소를 생성자로 넘겨받게 합니다. 부품을 안에서 만들지 않고 밖에서 받아 쓰는 이 방식을 의존성 주입이라고 부릅니다. 그래야 운영에서는 진짜를, 테스트에서는 페이크를 넘길 수 있습니다.
아래는 두 단계를 갖춘 가입 코드입니다.
interface MemberRepository {
void save(String email);
boolean exists(String email);
}
class SignupService {
private final MemberRepository repo;
SignupService(MemberRepository repo) {
this.repo = repo;
}
String signup(String email) {
if (repo.exists(email)) return "이미 가입됨";
repo.save(email);
return "가입 완료";
}
}
가입 코드는 MemberRepository 라는 약속만 압니다. exists 로 이메일이 있는지 묻습니다. 없으면 save 로 저장합니다. 약속 뒤에 데이터베이스가 있는지 메모리가 있는지는 모릅니다.
이제 메모리에 담는 페이크를 만듭니다. HashSet 은 같은 값을 두 번 담지 않는 자바의 집합입니다. 저장한 이메일을 여기에 넣어 둡니다. 물어보면 여기서 찾습니다.
class FakeMemberRepository
implements MemberRepository {
private final Set<String> emails =
new HashSet<>();
public void save(String email) {
emails.add(email);
}
public boolean exists(String email) {
return emails.contains(email);
}
}
몸통이 두 줄뿐이어도 진짜 저장소처럼 동작합니다. 저장한 이메일은 기억합니다. 저장하지 않은 이메일은 없다고 답합니다.
테스트는 페이크를 넘기고 가입을 세 번 부릅니다.
var repo = new FakeMemberRepository();
var s = new SignupService(repo);
s.signup("[email protected]"); // "가입 완료"
s.signup("[email protected]"); // "이미 가입됨"
s.signup("[email protected]"); // "가입 완료"
테스트는 답의 순서를 하나도 적지 않았습니다. 첫 번째 가입이 저장한 이메일을 페이크가 기억하고 있어서 두 번째 가입이 거절됩니다. 아래 그림은 두 번의 가입 사이에 페이크가 무엇을 기억하는지 보입니다.
sequenceDiagram
participant T as 테스트
participant S as 가입 코드
participant F as 페이크
T->>S: [email protected] 으로 가입
S->>F: exists 로 묻는다
F-->>S: 없다
S->>F: save 로 담는다
Note over F: 담아 둔 주소 [email protected]
S-->>T: 가입 완료
T->>S: 같은 주소로 또 가입
S->>F: exists 로 묻는다
F-->>S: 있다
S-->>T: 이미 가입됨
가입 코드가 저장소를 한 번 더 부르게 고쳐도 이 테스트는 깨지지 않습니다. 페이크는 몇 번 불리든 담아 둔 것을 기준으로 답하기 때문입니다.
페이크가 건너뛰는 일
페이크로 돌린 테스트가 금방 끝나는 까닭은 운영에 필요한 일을 건너뛰기 때문입니다. 진짜 저장소가 하는 일과 페이크가 하는 일을 견주면 아래와 같습니다.
| 진짜 데이터베이스 저장소 | 페이크 | |
|---|---|---|
| 값을 담는 곳 | 디스크 | 메모리 |
| 테스트 전 준비 | 서버를 띄우고 테이블을 만든다 | 객체를 하나 만든다 |
| 한 번 읽고 쓸 때 | 네트워크와 디스크를 거친다 | 메모리 안에서 끝난다 |
| 테스트가 끝나면 | 데이터가 남는다 | 객체와 함께 사라진다 |
마지막 줄이 특히 쓸모가 있습니다. 테스트마다 새 페이크를 만들면 앞 테스트가 남긴 데이터가 없습니다. 그래서 테스트끼리 서로의 결과를 흔들지 않습니다.
건너뛴 일 때문에 페이크는 운영에 못 씁니다. 프로그램이 꺼지면 저장한 회원이 모두 사라집니다. 여러 요청이 동시에 저장소를 건드릴 때의 처리도 대개 빠져 있습니다.
같은 방식으로 다른 부품도 페이크로 바꿉니다. 파일을 디스크 대신 메모리에 쓰는 파일 시스템, 메시지를 메모리 속 목록에 쌓아 두는 큐, 메모리에서만 도는 작은 데이터베이스가 흔한 예입니다.
스텁·목 객체와 가르는 기준
테스트 더블은 여럿입니다. 페이크를 다른 둘과 가르는 기준은 답을 어떻게 만드느냐입니다.
스텁은 앞에서 본 대로 테스트가 적어 둔 답을 돌려주는 가짜입니다. 목 객체는 어떤 메서드가 몇 번, 어떤 값으로 불렸는지를 확인하는 가짜입니다. 결과보다 부르는 방식이 중요한 테스트에서 씁니다.
| 이름 | 답을 만드는 방법 | 테스트가 확인하는 것 |
|---|---|---|
| 스텁 | 테스트가 적어 둔 답을 돌려준다 | 테스트 대상이 낸 결과 |
| 목 객체 | 미리 적어 둔 호출이 왔는지 맞춰 본다 | 테스트 대상이 부른 방식 |
| 페이크 | 받은 값을 담아 두고 거기서 답을 계산한다 | 테스트 대상이 낸 결과 |
스텁의 답은 테스트가 정합니다. 페이크의 답은 페이크 자신이 정합니다. 그래서 저장하고 다시 읽는 흐름처럼 앞 호출이 뒤 호출의 답을 바꿀 때도 테스트가 답 순서를 적을 필요가 없습니다.
페이크를 쓰는 테스트는 대개 테스트 대상이 낸 결과를 봅니다. 앞의 예에서도 「이미 가입됨」이 돌아왔는지만 봤습니다. 이런 확인을 상태 검증이라고 부릅니다. 저장소를 몇 번 불렀는지는 보지 않습니다.
페이크라는 말은 일상에서 「가짜」라는 뜻입니다. 그래서 문서에 따라 테스트 더블 전체를 느슨하게 페이크라고 부르기도 합니다. 이 항목의 페이크는 그중 스스로 동작하는 구현을 가진 갈래만 가리킵니다.
진짜와 어긋나는 문제
페이크는 진짜를 흉내 낸 별개의 코드입니다. 흉내가 빠진 대목에서는 진짜와 다르게 굽니다. 그 대목을 지나는 테스트는 통과해도 운영에서 깨집니다.
진짜 데이터베이스는 이메일 칸에 유니크 제약을 걸어 둘 수 있습니다. 유니크 제약은 같은 값이 두 번 저장되지 않게 데이터베이스가 막는 규칙입니다. 이 규칙이 걸린 저장소에 같은 이메일을 두 번 저장하면 오류가 납니다.
앞의 페이크는 같은 이메일을 두 번 저장해도 조용히 넘어갑니다. HashSet 이 중복을 한 개로 줄여 버리기 때문입니다.
가입 코드가 exists 확인을 빠뜨렸다고 해 봅시다. 같은 이메일로 두 번 가입하면 페이크는 두 번째 save 도 오류 없이 받습니다. 운영의 데이터베이스는 그 두 번째 save 에서 유니크 제약 오류를 냅니다. 오류가 나지 않는지만 보는 테스트라면 페이크로는 통과하고 운영에 가서야 깨집니다.
진짜 저장소가 바뀔 때도 어긋납니다. 진짜에 새 규칙이 생겼는데 페이크를 안 고치면 둘이 벌어집니다. 페이크도 유지해야 하는 코드라는 뜻입니다.
페이크를 진짜에 맞춰 두는 법
어긋남을 막는 흔한 방법은 같은 테스트 묶음을 진짜와 페이크 양쪽에 돌리는 것입니다. 「저장한 이메일은 있다고 답한다」·「같은 이메일을 두 번 저장하면 오류가 난다」 같은 테스트를 한 벌 짜 둡니다. 둘 다 이 묶음을 통과하면 적어도 그 범위에서는 같게 굽니다.
앞의 페이크는 이 묶음의 두 번째 테스트에서 떨어집니다. 그래서 save 가 이미 담긴 이메일을 받으면 오류를 내도록 페이크를 고칩니다.
진짜 저장소를 만든 쪽이 페이크도 함께 만들어 관리하는 방법도 씁니다. 진짜를 고친 사람이 페이크까지 같이 고치므로 벌어질 틈이 줄어듭니다.
코드 조각 하나를 떼어 시험하는 테스트를 단위 테스트라고 부릅니다. 페이크는 주로 단위 테스트에서 진짜를 대신합니다.
그래도 페이크로 짠 단위 테스트만으로는 부족합니다. 가입 코드와 진짜 데이터베이스가 맞물리는 곳은 페이크로 확인할 수 없기 때문입니다. 그래서 진짜 데이터베이스를 붙여 함께 돌리는 통합 테스트를 몇 개 따로 둡니다.
쓸 때와 안 쓸 때
페이크는 같은 부품을 여러 번 읽고 쓰는 흐름을 시험할 때 씁니다. 저장한 뒤 찾기, 고친 뒤 다시 읽기, 지운 뒤 없는지 보기가 그렇습니다. 스텁으로 답 순서를 적어 두기 번거로운 흐름입니다.
여러 테스트가 같은 부품을 쓸 때도 페이크를 씁니다. 페이크는 한 번 만들어 두면 테스트마다 다시 쓸 수 있습니다. 스텁은 테스트마다 답을 새로 적어야 합니다.
답 하나만 필요하면 스텁이 짧습니다. 「저장소가 오류를 낼 때 가입 코드가 어떻게 하나」를 보려면 오류 하나만 돌려주는 스텁이면 됩니다.
메일을 보냈는지처럼 부른 것 자체를 확인해야 하면 목 객체나 테스트 스파이를 씁니다. 테스트 스파이는 불린 호출을 기록해 두었다가 테스트가 나중에 꺼내 보게 하는 가짜입니다.
진짜를 붙여도 곧바로 답이 나오고 준비할 것이 없으면 진짜를 씁니다. 계산만 하는 클래스를 페이크로 바꾸면 얻는 것 없이 어긋날 틈만 늘어납니다. 유니크 제약처럼 진짜 부품의 세부 동작을 확인하려는 테스트도 진짜를 붙여서 돌립니다.
관련 항목
페이크와 나란히 서는 테스트 더블 갈래
테스트 더블 · 스텁 · 목 객체 · 테스트 스파이 · 더미 객체
페이크로 흔히 대신하는 부품
데이터베이스 · 인메모리 데이터베이스 · 저장소 패턴 · 메시지 큐 · 파일 시스템 · 시스템 시계
페이크를 끼울 틈을 만드는 설계 기법
의존성 주입 · 인터페이스 · 결합도 · 의존성 역전 원칙 · 다형성
페이크로 결과를 확인하는 방식
상태 검증 · 행위 검증 · 단언
페이크를 쓰는 테스트 종류
단위 테스트 · 통합 테스트 · 계약 테스트 · 테스트 피라미드 · 테스트 주도 개발
페이크 대신 고를 수 있는 다른 수단
스터빙 · 모킹 프레임워크 · Testcontainers · 서비스 가상화 · 목 서버
페이크가 줄여 주는 테스트 문제
느린 테스트 · 불안정한 테스트 · 깨지기 쉬운 테스트 · 테스트 격리
페이크가 속하는 상위 분류
QA와 테스트 · xUnit · 테스트 패턴 · 테스트 자동화
다른 이름: fake · fake object · Fake Object · 페이크 객체 · 가짜 객체