사전 목 객체
패턴

목 객체

gabury1고친 사람 github-actions[bot]

목 객체는 테스트하는 코드가 다른 부품을 옳게 불렀는지 지켜봅니다. 진짜 부품 대신 끼워 두면 어떤 메서드가 무슨 값으로 몇 번 불렸는지 확인합니다. 미리 적어 둔 호출과 다르게 불리면 테스트를 실패시킵니다. 메일 보내기처럼 돌려받는 값이 없는 일을 시험할 때 씁니다.

쉽고 빠른 이해

무슨 일을 하나 — 테스트할 때 진짜 부품 대신 끼워서, 코드가 그 부품을 제대로 불렀는지 봅니다. 회원 가입 코드를 시험하면서 진짜 메일 발송 부품 대신 끼우고 「가입한 주소로 환영 메일을 한 번 보냈나」를 확인하는 식입니다.

왜 하나 — 메일 보내기는 돌려주는 값이 없어서, 가입 코드의 결과만 봐서는 메일을 보냈는지 알 수 없습니다. 진짜 메일 서버를 붙이면 테스트를 돌릴 때마다 메일이 나갑니다.

어떻게 도나

  1. 테스트가 가짜에게 「이 주소로 한 번 불릴 것」이라고 적어 둡니다
  2. 가짜를 넘겨받은 가입 코드를 돌립니다
  3. 끝에 가짜에게 확인을 시킵니다. 적어 둔 대로 안 불렸으면 테스트가 실패합니다

대가 — 테스트가 코드의 결과가 아니라 부르는 방식에 묶입니다. 결과는 같게 두고 부르는 방식만 바꿔도 테스트가 깨집니다.

안 쓸 때 — 값만 묻는 호출에는 쓰지 않습니다. 돌려받은 값으로 낸 결과를 보면 확인되기 때문입니다.

상세

상세는 회원 가입 코드와 메일 발송 부품 하나로 끝까지 갑니다. 먼저 결과만 봐서는 확인할 수 없는 일이 무엇인지 봅니다. 그다음 목 객체를 손으로 만들어 끼우는 과정을 코드로 따라갑니다.

뒤쪽에서는 비슷한 가짜인 스텁·스파이와 무엇이 다른지 가르고, 실무에서 이 이름을 더 넓게 쓰는 사정을 짚습니다. 끝으로 쓰면 잃는 것과 쓸 때와 안 쓸 때를 봅니다.

결과로는 확인할 수 없는 일

회원 가입 코드를 시험한다고 합시다. 이 코드는 회원을 저장한 뒤 가입한 주소로 환영 메일을 보냅니다. 이렇게 시험하려는 코드가 테스트 대상입니다.

테스트 대상이 일을 하려고 불러 쓰는 다른 부품은 의존 객체입니다. 가입 코드에게는 메일 서버와 통신하는 메일 발송 부품이 의존 객체입니다.

메일 발송 메서드는 돌려주는 값이 없습니다. 가입 코드도 메일을 보낸 뒤 따로 남기는 값이 없습니다. 테스트가 가입 코드의 결과만 들여다봐서는 메일을 보냈는지, 누구에게 보냈는지 알 수 없습니다.

그렇다고 진짜 메일 발송 부품을 붙이면 테스트를 돌릴 때마다 실제 메일이 나갑니다. 메일 서버가 잠깐 멈추면 가입 코드가 멀쩡해도 테스트가 실패합니다.

그래서 메일 발송 부품 대신 가짜를 끼웁니다. 그 가짜가 어떻게 불렸는지를 봅니다. 테스트하는 동안 진짜 대신 끼우는 가짜를 통틀어 테스트 더블이라고 부릅니다. 목 객체는 그중 호출을 확인하는 갈래입니다.

결과를 보는 확인과 호출을 보는 확인

테스트 대상이 낸 결과나 남긴 값을 보는 확인을 상태 검증이라고 부릅니다. 「주문 금액 계산이 5000 원을 돌려줬나」가 그런 확인입니다. 대부분의 테스트는 이 방식으로 끝납니다.

테스트 대상이 의존 객체를 어떻게 불렀는지 보는 확인은 행위 검증이라고 부릅니다. 「메일 발송 메서드를 가입한 주소로 한 번 불렀나」가 그런 확인입니다. 목 객체는 이 행위 검증을 맡는 테스트 더블입니다.

상태 검증으로 끝나는 일이면 목 객체가 필요 없습니다. 메일 보내기처럼 호출하는 것 자체가 그 코드가 해야 할 일일 때 목 객체가 쓸모를 얻습니다.

목 객체를 끼울 틈

목 객체를 끼우려면 테스트 대상이 의존 객체를 스스로 만들지 않아야 합니다. 가입 코드 안에서 진짜 메일 발송 클래스를 new 로 만들면 가짜가 들어갈 틈이 없습니다. 두 코드가 이렇게 서로에게 기대는 정도가 결합도입니다. 결합도가 높을수록 가짜를 끼울 틈이 좁아집니다.

틈을 만드는 첫 단계는 메일 발송이 할 일을 인터페이스로 적어 두는 것입니다. 인터페이스는 메서드 이름과 모양만 정하고 몸통은 비워 둔 약속입니다. 진짜 부품과 목 객체가 같은 약속을 저마다 구현할 수 있게 됩니다.

둘째 단계는 가입 코드가 메일 발송 부품을 생성자로 넘겨받게 하는 것입니다. 부품을 안에서 만들지 않고 밖에서 받아 쓰는 이 방식을 의존성 주입이라고 부릅니다.

아래는 두 단계를 갖춘 가입 코드입니다.

Java
interface Mailer {
    void send(String to);
}

class SignupService {
    private final Mailer mailer;

    SignupService(Mailer mailer) {
        this.mailer = mailer;
    }

    void signup(String email) {
        // 회원 저장은 생략
        mailer.send(email);
    }
}

가입 코드는 Mailer 라는 약속만 압니다. 운영에서는 메일 서버와 통신하는 진짜 부품을 넘깁니다. 테스트에서는 목 객체를 넘깁니다.

받을 호출을 먼저 적어 두는 방식

테스트는 시작할 때 목 객체에게 받을 호출을 미리 적어 줍니다. 어느 메서드가 어떤 인자로 몇 번 불릴지를 적습니다. 이렇게 미리 적어 둔 호출을 기대라고 부릅니다. 목 객체는 테스트 대상이 돈 뒤 실제로 받은 호출을 기대와 맞춰 봅니다.

순서는 아래 그림과 같습니다. 테스트가 기대 적기 · 가입 부르기 · 확인 시키기를 차례로 합니다.

sequenceDiagram
    participant T as 테스트
    participant S as 가입 코드
    participant M as 목 객체
    T->>M: 기대를 적어 둔다
    T->>S: 목 객체를 넘기고 가입을 부른다
    S->>M: send 를 부른다
    T->>M: 확인을 시킨다
    M-->>T: 기대와 다르면 테스트를 실패시킨다

맞춰 본 결과가 다르면 목 객체가 스스로 테스트를 실패시킵니다. 기대한 호출이 안 왔을 때도, 기대와 다른 호출이 왔을 때도 그렇습니다. 판정을 테스트 코드가 아니라 목 객체가 내린다는 점이 목 객체를 다른 테스트 더블과 가릅니다.

아래는 도구 없이 손으로 만든 목 객체입니다. expectSend 로 기대를 적습니다. send 가 불리면 받는 주소를 기대와 맞춰 봅니다. verify 로 불린 횟수를 확인합니다.

Java
class MockMailer implements Mailer {
    private String expected;
    private int calls = 0;

    void expectSend(String to) {
        expected = to;
    }

    public void send(String to) {
        calls++;
        if (!to.equals(expected))
            throw new AssertionError("다른 주소");
    }

    void verify() {
        if (calls != 1)
            throw new AssertionError("횟수 " + calls);
    }
}

AssertionError 를 던지면 테스트 도구가 그 테스트를 실패로 셉니다. 이렇게 조건이 틀렸을 때 테스트를 실패시키는 문장을 단언이라고 부릅니다. 이 목 객체는 단언을 자기 안에 품고 있습니다.

테스트는 세 줄입니다. 기대를 적습니다. 가입을 부릅니다. 확인을 시킵니다.

Java
var mock = new MockMailer();
mock.expectSend("[email protected]");

new SignupService(mock).signup("[email protected]");
mock.verify();            // 통과

이번에는 가입 코드가 메일 보내기를 빠뜨렸다고 합시다. send 가 한 번도 안 불리므로 마지막 확인에서 테스트가 실패합니다.

Java
mock.verify();   // AssertionError: 횟수 0

가입 코드가 엉뚱한 주소로 메일을 보내면 더 일찍 실패합니다. send 가 불리는 순간 기대한 주소와 맞춰 보기 때문입니다.

스텁·스파이와 가르는 기준

목 객체와 자주 헷갈리는 테스트 더블이 둘 있습니다. 셋은 무엇을 확인하는지와 누가 판정을 내리는지로 갈립니다.

이름 하는 일 판정을 내리는 쪽
스텁 정해 둔 답을 돌려준다. 호출은 확인하지 않는다 테스트 코드가 테스트 대상의 결과를 본다
테스트 스파이 답을 주면서 불린 횟수와 인자를 적어 둔다 테스트 코드가 그 기록을 꺼내 본다
목 객체 미리 적어 둔 기대와 실제 호출을 맞춰 본다 목 객체가 스스로

스텁은 상태 검증을 돕습니다. 스파이와 목 객체는 행위 검증을 돕습니다. 스파이와 목 객체는 둘 다 호출을 봅니다. 둘을 가르는 것은 기대를 먼저 적어 두느냐, 판정을 누가 내리느냐입니다.

넓게 쓰이는 「목」

실무에서는 목이라는 말을 이보다 넓게 씁니다. 테스트 더블을 종류와 상관없이 모두 목이라고 부르는 일이 흔합니다.

이런 쓰임은 도구 이름에서도 보입니다. 테스트 더블을 코드 몇 줄로 만들어 주는 라이브러리를 모킹 프레임워크라고 부릅니다. 인터페이스만 주면 가짜 구현을 실행 중에 만들어 줍니다. 그 가짜를 대개 목이라고 부릅니다.

이런 도구가 만든 목은 쓰는 방법에 따라 성격이 바뀝니다. 답만 정해 두면 스텁처럼 쓰입니다. 불린 뒤에 호출을 꺼내 확인하면 스파이처럼 쓰입니다. 기대를 먼저 적어 두지 않는 쓰임도 목이라고 부르는 셈입니다.

다른 문서에서 목을 만나면 앞 표의 기준으로 가려 읽습니다. 기대를 먼저 적고 가짜가 판정했으면 좁은 뜻의 목 객체입니다. 불린 뒤에 테스트가 기록을 꺼내 확인했으면 스파이입니다. 답만 돌려줬다면 스텁입니다.

쓰면 잃는 것

목 객체를 쓴 테스트는 테스트 대상이 안에서 무엇을 부르는지에 묶입니다. Mailer 에 여러 주소로 한 번에 보내는 sendAll 이 더해졌다고 합시다. 가입 코드가 send 대신 sendAll 로 같은 메일을 보내게 고칩니다. 나가는 메일은 같습니다. 그래도 send 를 기대한 목 객체는 테스트를 실패시킵니다. 기대한 호출이 안 왔기 때문입니다.

동작은 두고 코드 구조만 바꾸는 일을 리팩터링이라고 부릅니다. 방금 sendAll 로 바꾼 것도 리팩터링입니다. 목 객체가 많은 테스트는 리팩터링할 때마다 함께 고쳐야 합니다.

코드가 옳은데도 이렇게 자주 깨지는 테스트를 깨지기 쉬운 테스트라고 부릅니다. 깨질 때마다 테스트를 고치는 품이 듭니다. 실패가 떠도 진짜 결함인지부터 따져야 합니다.

테스트에 꼭 필요하지 않은 호출까지 기대로 적어 두면 이 문제가 커집니다. 확인할 필요가 없는 세부까지 테스트가 정해 두는 것을 과잉 명세라고 부릅니다.

목 객체는 진짜 메일 서버가 어떻게 구는지 모릅니다. 진짜 서버가 형식이 틀린 주소를 거부해도, 목 객체는 적어 둔 호출이 왔는지만 봅니다. 테스트는 통과해도 운영에서는 깨질 수 있습니다.

코드 조각 하나를 떼어 시험하는 테스트를 단위 테스트라고 부릅니다. 목 객체는 주로 단위 테스트에서 진짜 부품을 대신합니다. 목 객체로 짠 단위 테스트만으로는 부품 사이의 이음매를 확인할 수 없습니다.

진짜 부품을 붙여 조각 사이의 이음매를 확인하는 테스트는 통합 테스트입니다. 형식이 틀린 주소 같은 문제는 여기서 드러납니다. 단위 테스트 옆에 통합 테스트를 몇 개 따로 두는 까닭입니다.

쓸 때와 안 쓸 때

코드가 값을 돌려주는 것 말고 바깥에 남기는 변화를 부수 효과라고 부릅니다. 메일이나 알림 보내기, 결제 서버에 결제 요청하기가 그렇습니다. 목 객체는 이런 부수 효과를 일으키는 호출을 확인할 때 씁니다.

값을 묻기만 하는 호출에는 목 객체를 쓰지 않습니다. 회원 정보를 조회하는 호출은 스텁으로 답만 정해 줍니다. 그다음 테스트 대상이 그 답으로 낸 결과를 봅니다. 결과가 같다면 조회를 몇 번 했는지는 상관없습니다. 그것까지 확인하면 과잉 명세가 됩니다.

계산만 하는 클래스나 값만 담는 객체는 가짜로 바꾸지 않고 진짜를 씁니다. 늘 같은 답을 곧바로 내므로 바꿔서 얻는 것이 없습니다. 가짜로 바꾸면 가짜가 진짜와 어긋날 틈만 늘어납니다.

관련 항목

목 객체와 나란히 서는 테스트 더블 갈래

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

목 객체로 결과를 확인하는 방식

행위 검증 · 상태 검증 · 단언 · 명령 조회 분리 · 부수 효과

목 객체를 만드는 도구와 기법

모킹 프레임워크 · Mockito · 스터빙 · 몽키 패칭 · 동적 프록시

목 객체를 끼울 틈을 만드는 설계 기법

의존성 주입 · 인터페이스 · 결합도 · 의존성 역전 원칙 · 제어의 역전

목 객체를 쓰는 테스트 종류

단위 테스트 · 통합 테스트 · 계약 테스트 · 테스트 주도 개발

목 객체를 잘못 쓸 때 생기는 테스트 문제

깨지기 쉬운 테스트 · 과잉 명세 · 불안정한 테스트 · 테스트 냄새 · 리팩터링

목 객체와 이름이나 역할이 겹치는 이웃

목 서버 · 목업 · 서비스 가상화 · 샌드박스

목 객체가 속하는 상위 분류

QA와 테스트 · xUnit · 테스트 자동화 · 테스트 패턴

다른 이름: mock object · Mock Object · mock · 목 · 모의 객체 · 목 오브젝트