사전 모킹 프레임워크
개념

모킹 프레임워크

gabury1고친 사람 github-actions[bot]

모킹 프레임워크는 테스트에 쓸 가짜 객체를 코드 몇 줄로 만들어 줍니다. 가짜가 불렸을 때 무엇을 돌려줄지 정하게 해 줍니다. 테스트가 끝나면 가짜가 어떻게 불렸는지 확인하게 해 줍니다. 가짜 클래스를 손으로 하나씩 짜는 수고를 덜어 주는 도구입니다.

쉽고 빠른 이해

모킹 프레임워크는 테스트용 가짜 객체를 대신 만들어 주는 라이브러리입니다. 재고 저장소 인터페이스를 넘기면 「사과 재고는 3개」라고 답하는 가짜가 한 줄로 생깁니다.

이게 없으면 가짜가 필요할 때마다 클래스를 손으로 짜야 합니다. 메서드가 열 개인 인터페이스라면 안 쓰는 아홉 개까지 몸통을 채워야 합니다. 몇 번 불렸는지 세려면 세는 코드도 직접 넣어야 합니다.

  1. 인터페이스나 클래스를 넘겨 가짜를 만듭니다
  2. 어떤 호출에 무엇을 돌려줄지 적습니다
  3. 테스트할 코드를 돌린 뒤 가짜가 어떻게 불렸는지 확인합니다

대가는 가짜가 진짜와 달라도 테스트가 통과한다는 것입니다. 호출을 확인하는 테스트는 결과가 같아도 부르는 방식만 바뀌면 깨집니다.

상세

영화 촬영장의 소품 담당을 떠올리면 됩니다. 감독이 「이 장면엔 울리는 전화기가 필요하다」고 말하면 소품 담당이 맞는 전화기를 바로 내줍니다. 배우는 그 전화기를 들고 통화 장면을 찍습니다.

모킹 프레임워크는 테스트의 소품 담당입니다. 테스트가 「이런 협력 객체가 필요하다」고 말하면 맞는 가짜를 바로 만들어 줍니다. 이 절은 주문을 받는 OrderService 하나를 끝까지 예로 씁니다. 가짜를 손으로 만들 때의 수고에서 시작해 도구가 가짜를 찍어 내는 방법과 그 대가까지 차례로 봅니다.

협력 객체와 테스트 더블

OrderService 처럼 이번 테스트에서 시험하려는 코드를 테스트 대상 코드라고 부릅니다. OrderService 는 주문을 받을지 정하려고 재고 저장소에 재고를 묻습니다. 주문이 끝나면 메일 발송기에 확인 메일을 보내라고 시킵니다. 이렇게 테스트 대상 코드가 일을 하려고 부르는 다른 객체를 협력 객체라고 부릅니다.

협력 객체는 대개 바깥 세계와 맞닿아 있습니다. 재고 저장소는 데이터베이스를 읽습니다. 메일 발송기는 메일 서버에 접속합니다. 테스트에서 이것들을 그대로 쓰면 테스트가 느려집니다. 돌릴 때마다 진짜 메일도 나갑니다.

그래서 테스트에서는 협력 객체를 가짜로 바꿔 끼웁니다. 진짜 대신 테스트에 끼우는 가짜를 통틀어 테스트 더블이라고 부릅니다. 영화에서 배우 대신 위험한 장면을 찍는 대역에서 따온 이름입니다.

테스트 더블 가운데 정해 둔 답만 돌려주는 것을 스텁이라고 부릅니다. 불린 호출을 기록해 두었다가 나중에 확인하게 해 주는 것은 목이라고 부릅니다.

모킹 프레임워크라는 이름은 이 목에서 왔습니다. 그런데 이 도구가 만드는 가짜는 스텁 노릇도 하고 목 노릇도 합니다. 실무에서 테스트 더블을 종류와 상관없이 목이라고 부르는 일이 많은 것도 그래서입니다.

가짜를 끼울 틈

가짜를 끼우려면 OrderService 가 협력 객체를 스스로 만들지 않아야 합니다. 안에서 진짜 저장소를 new 로 만들면 테스트가 끼어들 틈이 없습니다. 그래서 협력 객체를 생성자로 넘겨받게 짭니다. 이 방식을 의존성 주입이라고 부릅니다.

넘겨받는 타입은 인터페이스로 둡니다. 인터페이스는 메서드의 이름과 모양만 정하고 몸통은 비워 둔 틀입니다. 진짜와 가짜가 같은 인터페이스를 구현하면 OrderService 는 둘을 구별하지 않습니다.

Java
interface StockRepository {
    int count(String item);
}

interface MailSender {
    void send(String to, String text);
}

class OrderService {
    private final StockRepository stock;
    private final MailSender mail;

    OrderService(StockRepository stock,
                 MailSender mail) {
        this.stock = stock;
        this.mail = mail;
    }

    boolean order(String item, String to) {
        if (stock.count(item) <= 0) {
            return false;
        }
        mail.send(to, item + " 주문 완료");
        return true;
    }
}

order 는 재고가 없으면 false 를 돌려주고 끝납니다. 재고가 있으면 확인 메일을 보낸 뒤 true 를 돌려줍니다. 시험할 것은 둘입니다. 재고에 따라 결과가 맞게 갈리는지, 메일이 맞는 주소로 나가는지입니다.

손으로 만드는 가짜의 수고

모킹 프레임워크 없이도 가짜는 만들 수 있습니다. 인터페이스를 구현하는 클래스를 테스트 코드 안에 하나 짜면 됩니다. 메일 발송기의 가짜라면 send 가 불릴 때마다 받은 주소를 목록에 쌓아 두게 짭니다.

Java
class RecordingMail implements MailSender {
    List<String> sent = new ArrayList<>();

    public void send(String to, String text) {
        sent.add(to);
    }
}

이 가짜를 넘기고 order 를 부른 뒤 sent 를 들여다보면 메일이 몇 통, 어디로 나갔는지 알 수 있습니다. 가짜 하나로는 짤 만합니다.

수고는 가짜가 늘어날 때 생깁니다. 테스트마다 원하는 답이 다르면 답마다 클래스를 새로 짜야 합니다. 메서드가 열 개인 인터페이스라면 안 쓰는 아홉 개도 몸통을 채워야 컴파일됩니다. 인자와 호출 횟수를 기록하는 코드는 가짜마다 되풀이됩니다.

모킹 프레임워크는 이 되풀이를 걷어 냅니다. 가짜 클래스는 도구가 만듭니다. 테스트에는 「무엇을 돌려줄지」와 「무엇을 확인할지」만 남습니다.

모킹 프레임워크가 하는 일 세 가지

도구마다 쓰는 모양은 달라도 하는 일은 셋으로 모입니다. 가짜를 만들고, 답을 정하고, 호출을 확인합니다.

하는 일 테스트가 적는 것 이 일의 이름
가짜 만들기 가짜로 만들 인터페이스나 클래스 모킹
답 정하기 「이 메서드가 이 인자로 불리면 이 값을 돌려줘라」 스터빙
호출 확인 「이 메서드가 이 인자로 한 번 불렸어야 한다」 호출 검증

둘째 줄은 테스트 대상 코드로 들어가는 값을 정합니다. 셋째 줄은 테스트 대상 코드에서 나가는 호출을 살핍니다. 그래서 재고 조회처럼 값을 묻는 협력 객체에는 스터빙을 씁니다. 메일 발송처럼 일을 시키는 협력 객체에는 호출 검증을 씁니다.

자바 코드로 본 모습

자바에서 널리 쓰는 모킹 프레임워크인 Mockito로 OrderService 를 시험해 봅니다. 아래 코드의 mock·when·verify 는 Mockito 가 주는 함수입니다.

Java
StockRepository stock =
    mock(StockRepository.class);
MailSender mail = mock(MailSender.class);
when(stock.count("사과")).thenReturn(3);

OrderService service =
    new OrderService(stock, mail);
service.order("사과", "[email protected]");  // true
service.order("배", "[email protected]");    // false

verify(mail).send(
    "[email protected]", "사과 주문 완료");

mock 을 두 번 불러 가짜 저장소와 가짜 메일 발송기를 만들었습니다. 클래스는 하나도 짜지 않았습니다. 그다음 when 줄이 스터빙입니다. 사과 재고를 물으면 3 을 답하라고 적었습니다.

배는 따로 정해 두지 않았습니다. 정해 두지 않은 호출에 이 가짜는 0 을 돌려줍니다. 그래서 배 주문은 재고가 없는 것으로 보고 false 가 됩니다.

마지막 verify 가 호출 검증입니다. 메일 발송기의 send 가 이 인자로 한 번 불렸는지 확인합니다. 한 번도 안 불렸거나 두 번 불렸으면 테스트가 실패합니다.

인자를 하나하나 적기 번거로우면 「아무 문자열이나」 같은 조건을 대신 적습니다. 이런 조건을 인자 매처라고 부릅니다. 메일 본문 문구가 이번 테스트의 관심사가 아닐 때 씁니다.

가짜가 호출을 받는 순서

가짜 안에서는 호출이 올 때마다 아래 그림의 순서가 돕니다. 기록을 먼저 남기고, 그다음 돌려줄 답을 고릅니다. 정해 둔 답이 없으면 빈 값을 돌려줍니다. 빈 값은 숫자면 0, 참거짓이면 false, 객체면 null 처럼 아무것도 없다는 뜻의 값입니다.

flowchart TD
    A["가짜에 호출이 온다"] --> B["메서드와 인자를 기록에 남긴다"]
    B --> C{"정해 둔 답이 있나"}
    C -->|있다| D["정해 둔 답을 돌려준다"]
    C -->|없다| E["빈 값을 돌려준다"]
    B -.-> F["검증 단계에서 이 기록을 읽는다"]

앞 예에서 사과 재고로 3 이 나온 것은 그림의 「있다」 갈래입니다. 배 재고로 0 이 나온 것은 「없다」 갈래입니다.

그림의 점선처럼 기록은 검증 단계에서 쓰입니다. verify 는 쌓인 기록을 뒤져 원하는 호출이 있었는지 봅니다. 한 가짜 위에서 스터빙과 호출 검증을 같이 할 수 있는 것은 기록과 답을 한곳에 두기 때문입니다.

정해 두지 않은 호출을 다루는 방식은 도구와 설정마다 다릅니다. 앞 예처럼 빈 값을 돌려주고 넘어가는 가짜를 너그러운 목이라고 부릅니다. 정해 두지 않은 호출이 오면 바로 테스트를 실패시키는 가짜는 엄격한 목이라고 부릅니다.

엄격한 목은 뜻밖의 호출을 잡아 줍니다. 그 대신 코드가 조금만 바뀌어도 테스트가 깨집니다. 너그러운 목은 덜 깨지지만 빠뜨린 스터빙을 빈 값으로 덮어 버립니다.

가짜를 찍어 내는 방법

모킹 프레임워크가 만드는 가짜 클래스는 소스 코드 어디에도 없습니다. 테스트가 돌 때 도구가 새로 만듭니다. 만드는 방법은 그 언어가 실행 중에 무엇을 허락하느냐에 따라 갈립니다.

인터페이스를 가짜로 만들 때는 실행 중에 그 인터페이스를 구현하는 클래스를 새로 만듭니다. 이 클래스의 메서드는 전부 처리기 하나로 이어집니다. 어느 메서드가 불리든 그 처리기가 받아 기록하고 답을 찾습니다. 자바에는 이 일을 해 주는 동적 프록시가 표준 라이브러리에 들어 있습니다.

클래스를 가짜로 만들 때는 그 클래스를 상속한 하위 클래스를 만들어 메서드를 전부 덮어씁니다. 이 하위 클래스는 소스 코드를 거치지 않고 바이트코드로 곧장 만듭니다. 바이트코드는 자바 가상 머신이 읽어 실행하는 중간 형태의 코드입니다.

상속으로 만드는 방법은 상속을 막아 둔 클래스에는 통하지 않습니다. 그래서 도구마다 우회 방법을 따로 둡니다. 이미 메모리에 올라온 클래스의 바이트코드를 고쳐 쓰는 방법이 그 하나입니다.

파이썬·자바스크립트처럼 실행 중에 객체를 마음대로 고칠 수 있는 언어는 클래스를 만들 필요가 없습니다. 어떤 이름의 메서드를 불러도 받아 주고 기록하는 객체 하나면 됩니다. 인터페이스 선언이 없어도 이 객체를 협력 객체 대신 넘기면 가짜 노릇을 합니다.

이런 언어에서는 가짜를 넘기는 대신 이미 있는 함수를 바꿔치기하기도 합니다. 테스트가 도는 동안만 모듈 안의 함수를 가짜 함수로 갈아 끼우고, 끝나면 되돌립니다. 대상 코드가 협력 객체를 넘겨받지 않고 모듈 함수를 곧장 부를 때 쓰는 방법입니다. 실행 중에 남의 코드를 이렇게 바꿔 끼우는 일의 이름이 몽키 패칭입니다.

Go 처럼 실행 중에 인터페이스를 구현하는 새 타입을 만들 수 없는 언어는 가짜를 미리 코드로 찍어 냅니다. 도구가 인터페이스 선언을 읽고 가짜 구현의 소스 코드를 써 줍니다. 그 파일은 다른 테스트 코드와 함께 컴파일됩니다. 이 방식이 코드 생성입니다.

언어마다 널리 쓰이는 도구와 그 도구가 가짜를 만드는 방법을 모으면 이렇습니다.

언어 도구 가짜를 만드는 방법
자바 Mockito · EasyMock 실행 중에 클래스를 만들거나 고친다
[[Kotlin 코틀린]] MockK
파이썬 unittest.mock 무엇이든 받아 주는 객체 · 함수 바꿔치기
자바스크립트 Jest 가짜 함수 · 모듈 바꿔치기
C# Moq 실행 중에 클래스를 만든다
Go gomock 가짜 코드를 미리 생성한다

파이썬은 이 도구를 표준 라이브러리에 넣어 두었습니다. Jest 는 테스트를 돌리는 도구입니다. 가짜 함수를 만드는 기능이 그 안에 같이 들어 있습니다.

모킹 프레임워크의 대가

첫째 대가는 가짜가 진짜와 어긋나도 테스트가 통과한다는 것입니다. 진짜 저장소는 없는 상품을 물으면 예외를 던진다고 해 봅시다. 가짜는 같은 물음에 0 을 답합니다. 테스트는 통과합니다. 운영에서는 예외가 터집니다.

가짜의 답은 테스트를 쓴 사람이 믿는 진짜의 동작입니다. 그 믿음이 틀리면 테스트는 틀린 믿음을 확인할 뿐입니다. 그래서 가짜로 짠 단위 테스트 말고도 진짜 협력 객체를 붙여 돌리는 통합 테스트를 따로 둡니다.

둘째 대가는 테스트가 구현에 묶인다는 것입니다. 호출 검증은 대상 코드가 어느 메서드를 어떤 인자로 불렀는지 따집니다. 결과가 같아도 부르는 방식이 바뀌면 테스트가 깨집니다. 기능은 멀쩡한데 구조만 바꿔도 깨지는 테스트를 깨지기 쉬운 테스트라고 부릅니다.

셋째 대가는 가짜가 너무 쉽게 만들어진다는 것입니다. 한 줄이면 생기니 협력 객체를 전부 가짜로 바꾸고 싶어집니다. 준비 코드가 테스트 본문보다 길어지면 무엇을 시험하는지 읽기 어려워집니다. 이런 상태를 과도한 모킹이라고 부릅니다.

과도한 모킹은 테스트만의 문제로 끝나지 않습니다. 가짜가 여럿 필요하다는 것은 대상 코드가 너무 많은 것에 기대고 있다는 뜻이기도 합니다. 그래서 준비 코드가 길어지면 대상 코드를 쪼갤 신호로 읽습니다.

쓰는 때와 안 쓰는 때

모킹 프레임워크는 협력 객체가 바깥 세계와 맞닿아 있을 때 씁니다. 데이터베이스, 외부 서비스, 파일, 현재 시각이 그렇습니다. 타임아웃이나 빈 결과처럼 진짜로는 일으키기 어려운 상황을 만들 때도 씁니다.

협력 객체가 메모리 안에서만 도는 단순한 객체면 가짜로 바꾸지 않습니다. 금액 계산 객체나 날짜 변환 함수는 진짜를 써도 빠릅니다. 결과도 늘 같습니다. 이런 것을 가짜로 바꾸면 대가만 치르고 얻는 것이 없습니다.

남이 만든 라이브러리의 타입을 곧장 가짜로 만드는 것도 피하는 편입니다. 그 가짜의 답에는 「이 라이브러리는 이렇게 돌 것이다」라는 추측이 들어갑니다. 테스트마다 가짜를 만들면 그 추측이 테스트 여기저기에 흩어집니다.

그래서 자기 코드에 인터페이스를 하나 두고, 라이브러리 호출은 그 인터페이스의 구현 안에서만 합니다. 이 구현은 라이브러리 호출을 감싸기만 하고 다른 일은 하지 않습니다. 테스트에서는 이 인터페이스를 가짜로 만듭니다. 인터페이스가 무엇을 돌려줄지는 자기가 정한 약속이라 가짜는 그 약속만 따르면 됩니다.

라이브러리에 대한 추측은 이제 그 구현 한 곳에만 남습니다. 그 구현이 라이브러리와 맞게 붙는지는 통합 테스트에서 따로 봅니다.

관련 항목

모킹 프레임워크가 만들어 주는 테스트 더블

테스트 더블 · 목 객체 · 스텁 · 스파이 · 페이크 · 더미

모킹 프레임워크를 구현한 라이브러리

Mockito · EasyMock · MockK · unittest.mock · Jest · Moq · gomock · Sinon.js

모킹 프레임워크가 가짜를 찍어 내는 기법

동적 프록시 · 리플렉션 · 바이트코드 · 상속 · 몽키 패칭 · 코드 생성

모킹 프레임워크가 제공하는 테스트 기능

스터빙 · 호출 검증 · 인자 매처 · 상태 검증 · 행위 검증

모킹 프레임워크를 쓰는 프로그래밍 언어

Java · Kotlin · Python · JavaScript · Go

모킹 프레임워크가 쓰이는 테스트 종류

단위 테스트 · 통합 테스트 · 테스트 격리 · 테스트 픽스처 · QA와 테스트

모킹 프레임워크가 기대는 설계 수단

의존성 주입 · 인터페이스 · 협력 객체 · 생성자 주입 · 의존성 역전 원칙

모킹 프레임워크를 과하게 쓸 때 생기는 테스트 문제

과도한 모킹 · 깨지기 쉬운 테스트 · 불안정한 테스트 · 테스트 냄새

다른 이름: mocking framework · mock framework · mocking library · 모킹 라이브러리 · 목 프레임워크