사전 스터빙
개념

스터빙

gabury1고친 사람 github-actions[bot]

스터빙은 테스트 중에 진짜 부품 대신 끼운 가짜가 무엇을 돌려줄지 미리 정해 두는 일입니다. 테스트할 코드는 그 답을 진짜 답으로 알고 움직입니다. 덕분에 데이터베이스나 외부 서버 없이도 원하는 상황을 골라 시험할 수 있습니다.

쉽고 빠른 이해

스터빙은 가짜 부품에게 「이렇게 물으면 이렇게 답해라」를 적어 주는 일입니다. 권한 서비스가 「그런 사용자 없음」이라고 답하게 적어 두면, 접근을 막는 코드가 제대로 막는지 진짜 서비스 없이 시험할 수 있습니다.

이게 없으면 테스트가 진짜 부품에 매달립니다. 진짜 부품은 느릴 수 있습니다. 테스트에 필요한 답을 마음대로 내주지 않습니다. 진짜 결제 서버처럼 부르기만 해도 곤란한 일이 생기는 부품도 있습니다.

  1. 진짜 부품과 같은 모양의 가짜를 만듭니다
  2. 가짜가 돌려줄 값이나 던질 예외를 적어 둡니다
  3. 가짜를 끼운 채 코드를 돌리고 결과를 확인합니다

대가는 가짜가 진짜와 어긋나도 테스트가 모른다는 것입니다. 가짜의 답을 많이 정해 둘수록 테스트가 코드 속사정에 묶여 자주 깨집니다. 그래서 진짜 부품이나 진짜와 비슷하게 도는 간단한 구현을 쓸 수 있으면 대개 그쪽을 먼저 씁니다.

상세

드라마 대본 리딩에서 아직 섭외되지 않은 배역이 있다고 해 봅시다. 조연출이 그 배역의 대사를 대신 읽습니다. 읽을 대사는 대본에 미리 적혀 있습니다. 주연 배우는 그 대사를 받아 자기 연기를 끝까지 맞춰 봅니다.

테스트에 끼운 가짜의 메서드는 스스로는 아무 동작도 하지 않습니다. 스터빙은 이런 함수에 동작을 주는 일입니다. 그 함수가 불렸을 때 어떤 값을 돌려줄지 테스트가 하나하나 정해 줍니다.

예를 들어 권한 서비스의 「사용자 조회」 메서드를 흉내 낸 가짜가 늘 빈 값을 돌려주게 정해 둘 수 있습니다. 그러면 「사용자를 못 찾으면 접근을 막는다」는 코드를 바로 시험할 수 있습니다.

테스트 대상 코드와 협력 객체

이번 테스트에서 시험하려는 코드를 테스트 대상 코드라고 합니다. 위 예에서는 사용자에게 접근 권한이 있는지 판단하는 코드입니다.

테스트 대상 코드는 혼자 일하지 않습니다. 접근 판단 코드는 사용자 정보를 권한 서비스에 물어봅니다. 이렇게 테스트 대상 코드가 일을 하려고 기대는 다른 객체를 협력 객체라고 부릅니다.

협력 객체가 돌려준 값은 테스트 대상 코드의 판단을 바꿉니다. 테스트가 인자로 직접 넘긴 값이 아닌데도 결과를 좌우합니다. 이런 값을 간접 입력이라고 합니다. 권한 서비스가 돌려준 「사용자 없음」이 접근 판단 코드의 간접 입력입니다.

스터빙은 이 간접 입력을 테스트가 손에 쥐는 방법입니다. 진짜 협력 객체를 테스트용 객체로 바꿔 끼웁니다. 그 객체가 테스트가 원하는 간접 입력을 테스트 대상 코드에 흘려 넣습니다.

스텁과 스터빙

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

테스트 더블 가운데 정해 둔 답을 돌려주는 것을 스텁이라고 합니다. 스텁은 물건의 이름입니다.

스터빙은 함수에 답을 적어 넣는 행위의 이름입니다. 답을 받는 함수가 스텁의 것이든 다른 테스트 더블의 것이든 상관없습니다. 「반환값을 스텁한다」처럼 동사로도 씁니다.

스터빙이 도는 순서

스터빙을 쓰는 테스트는 네 단계를 거칩니다. 그 전에 낱말 하나를 짚습니다. 인터페이스는 메서드의 이름과 모양만 정해 두고 몸통은 비워 둔 틀입니다.

  1. 테스트 대상 코드가 기대는 인터페이스를 테스트 전용으로 구현합니다
  2. 그 구현이 불렸을 때 돌려줄 값이나 던질 예외를 정해 둡니다
  3. 테스트 대상 코드를 돌리기 전에 진짜 대신 이 스텁을 끼웁니다
  4. 테스트 대상 코드를 돌리고 결과를 평소처럼 확인합니다

아래 그림은 권한 서비스 스텁을 끼운 접근 판단 테스트입니다.

sequenceDiagram
    participant 테스트
    participant 대상 as 접근 판단 코드
    participant 스텁 as 권한 서비스 스텁
    테스트->>스텁: 사용자를 물으면 빈 값을 답해라
    테스트->>대상: 스텁을 끼우고 접근 가능한지 묻는다
    대상->>스텁: 이 사용자 정보를 줘
    스텁-->>대상: 빈 값
    대상-->>테스트: 접근 불가
    Note over 테스트: 접근 불가가 맞는지 확인한다

셋째 화살표에서 접근 판단 코드는 상대가 스텁인 줄 모릅니다. 진짜 서비스에 묻듯이 묻습니다. 돌아온 답으로 평소처럼 판단합니다.

바꿔 끼울 틈

스텁이 진짜로 행세하려면 진짜와 같은 메서드 이름과 모양만 갖추면 됩니다. 진짜와 똑같이 동작할 필요는 없습니다. 테스트 대상 코드가 그 메서드만 부르기 때문입니다.

대신 테스트 대상 코드에 가짜를 끼울 길이 있어야 합니다. 그 길 가운데 하나가 의존성 주입입니다. 협력 객체를 코드 안에서 만드는 대신 생성자 같은 통로로 밖에서 넘겨받게 짜는 방식입니다. 이미 만든 객체의 메서드를 테스트 중에 바로 가짜로 바꿔 끼우는 길도 있습니다.

아래 그림은 의존성 주입으로 끼우는 꼴입니다. 접근 판단 코드는 권한 조회 인터페이스만 부릅니다.

flowchart TD
    대상["접근 판단 코드"] -->|부른다| 틀["권한 조회 인터페이스"]
    subgraph 운영
        진짜["진짜 권한 서비스"]
    end
    subgraph 테스트
        스텁["권한 서비스 스텁"]
    end
    진짜 -.->|구현한다| 틀
    스텁 -.->|구현한다| 틀

운영에서는 진짜 권한 서비스를 넘깁니다. 테스트에서는 스텁을 넘깁니다. 접근 판단 코드는 권한 조회 인터페이스만 보므로 둘을 구별하지 않습니다.

정해 두는 답의 네 모양

스터빙으로 정하는 답은 고정 값 하나에 그치지 않습니다. 테스트가 만들려는 상황에 따라 네 모양을 씁니다.

답의 모양 만드는 상황
늘 같은 값 「사용자 없음」처럼 한 가지 상태를 고정한다
입력마다 다른 값 사용자 A 는 있고 B 는 없는 것처럼 인자에 따라 답을 가른다
예외 협력 객체가 실패하는 경우를 만든다
부를 때마다 다른 값 첫 호출은 실패하고 둘째 호출은 성공하는 경우를 만든다

정상 값을 흘려 넣어 테스트 대상 코드가 제 일을 끝까지 하게 만드는 스텁이 있습니다. 이런 스텁을 응답자(Responder)라고 부릅니다. 기능이 잘 도는 정상 경로를 시험할 때 흔히 씁니다.

반대로 엉뚱한 값을 돌려주거나 예외를 던지는 스텁이 있습니다. 이런 스텁은 흔히 방해꾼(Saboteur)이라고 합니다. 테스트 대상 코드가 하려는 일을 일부러 틀어 놓습니다. 그 상황을 테스트 대상 코드가 어떻게 견디는지 봅니다.

손으로 쓰는 스텁과 모킹 프레임워크

스텁은 라이브러리 없이 손으로도 만듭니다. 인터페이스를 구현하는 클래스를 하나 새로 짭니다. 메서드마다 돌려줄 값을 박아 넣습니다. 메서드가 하나뿐인 인터페이스라면 람다 한 줄로도 됩니다. 람다는 이름 없이 바로 쓰는 함수입니다.

인터페이스의 메서드가 많아지면 이런 클래스를 짜는 코드가 길어집니다. 이 반복을 줄이려고 스터빙은 대개 모킹 프레임워크로 합니다. 모킹 프레임워크는 인터페이스를 받아 가짜 객체를 만들어 줍니다. 그 가짜에 「이 메서드가 이 인자로 불리면 이 값을 돌려줘라」를 한 줄로 적게 해 줍니다.

아직 없는 협력 객체를 대신하는 스텁

지금까지 본 스텁은 이미 있는 진짜 협력 객체를 테스트에서 바꿔 끼웠습니다. 진짜가 아직 만들어지지 않았을 때 그 대신 세워 두는 스텁도 있습니다. 대개 진짜 클래스의 빈 껍데기에 고정 값을 돌려주는 문장만 박아 둔 꼴입니다. 진짜가 준비되면 바로 진짜로 바꿉니다.

이런 스텁은 테스트 주도 개발에서 자주 생깁니다. 테스트 주도 개발은 테스트를 먼저 쓴 뒤 그 테스트를 통과시킬 코드를 짜는 방식입니다.

이 방식으로 부르는 쪽 코드부터 짜 들어가면, 그 코드가 부를 클래스는 아직 없습니다. 그 클래스를 빈 껍데기로 먼저 세웁니다. 껍데기에 코드를 채워 가다 보면 껍데기가 진짜 클래스가 됩니다.

이 쓰임까지 「테스트 스텁」이라고 부르는 사람이 있습니다. 앞의 쓰임과 헷갈리지 않게 「임시 테스트 스텁(Temporary Test Stub)」이라는 이름을 따로 쓰기도 합니다. 두 쓰임은 진짜가 할 일을 정해 둔 답으로 대신한다는 점에서 같습니다.

배경

테스트 대상 코드는 다른 부품에 기댑니다. 그런데 그 부품을 테스트 환경에서 쓰기 어려울 때가 있습니다. 아직 만들어지지 않았거나 테스트에 필요한 결과를 내주지 않는 경우입니다. 외부 신용카드 서버처럼 부르기만 해도 진짜 결제가 걸리는 부품도 있습니다. 진짜 부품을 거치는 테스트는 느리기도 합니다.

그러면 협력 객체의 답에 따라 갈리는 코드가 시험되지 않은 채 남습니다. 「사용자를 못 찾으면 거절한다」는 분기를 보려면 권한 서비스가 「없음」을 돌려줘야 합니다. 진짜 서비스에게서 그 답을 원하는 때에 받아 낼 방법이 마땅치 않습니다. 테스트가 간접 입력을 통제할 길이 없는 것입니다.

테스트에는 간접 입력을 정해 넣을 통제 지점이 필요했습니다. 진짜 협력 객체를 테스트 전용 객체로 바꿉니다. 그 객체가 돌려줄 값은 테스트가 미리 적어 둡니다. 답을 적어 두는 이 일이 스터빙입니다. 덕분에 테스트는 테스트 대상 코드를 평소라면 안 지나갈 경로로도 몰아넣을 수 있습니다.

예시

같은 일을 도구마다 어떻게 적는지 네 언어의 도구로 봅니다. 자바의 Mockito, 파이썬의 unittest.mock, 자바스크립트의 Jest, 루비의 RSpec 입니다.

Mockito

자바의 모킹 프레임워크입니다. mock() 으로 가짜를 만듭니다. 답은 when(...).thenReturn(...) 으로 정합니다.

Java
//You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class);

//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

//following prints "first"
System.out.println(mockedList.get(0));

//following throws runtime exception
System.out.println(mockedList.get(1));

//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

get(0) 에는 값을, get(1) 에는 예외를 정했습니다. 답을 정하지 않은 get(999) 는 null 을 돌려줍니다. 값을 돌려주는 메서드는 스터빙하지 않으면 null, 기본형 값, 빈 컬렉션 가운데 맞는 것을 돌려줍니다. int 면 0, boolean 이면 false 입니다.

한 번 정한 답은 몇 번을 불러도 같은 값으로 돌아옵니다. 부를 때마다 다른 답이 필요하면 답을 이어 붙입니다.

Java
when(mock.someMethod("some arg"))
  .thenThrow(new RuntimeException())
  .thenReturn("foo");

//First call: throws runtime exception:
mock.someMethod("some arg");

//Second call: prints "foo"
System.out.println(mock.someMethod("some arg"));

//Any consecutive call: prints "foo" as well (last stubbing wins).
System.out.println(mock.someMethod("some arg"));

첫 호출은 예외를 던집니다. 둘째부터는 "foo" 를 돌려줍니다. 마지막에 정한 답이 그 뒤의 호출에도 이어집니다.

이어 붙이는 스터빙이 쓸모 있는 경우는 드뭅니다. 대표적인 쓰임으로 꼽히는 것은 반복자 흉내입니다. 반복자는 부를 때마다 다음 원소를 하나씩 내주는 객체입니다. 반복자 대신 컬렉션을 넘길 수도 있습니다.

진짜 컬렉션을 쓰는 것이 자연스러운 스터빙 방법입니다. LinkedList 같은 리스트는 실무에서 가짜로 만들지 말고 진짜 인스턴스를 쓰기를 권합니다. 앞 예제가 리스트를 가짜로 만든 것은 대부분이 그 인터페이스를 익히 알아 문법이 눈에 빨리 들어오기 때문입니다.

unittest.mock

파이썬 표준 라이브러리에 든 도구입니다. return_value 로 값을 정합니다. 예외나 바뀌는 답은 side_effect 로 정합니다.

Python
>>> thing = ProductionClass()
>>> thing.method = MagicMock(return_value=3)
>>> thing.method(3, 4, 5, key='value')
3

return_value=3 을 준 메서드는 어떤 인자로 불러도 3 을 돌려줍니다. thing 은 이미 만든 진짜 객체입니다. 그 메서드 하나만 가짜로 바꿔 끼웠습니다.

예외를 정할 때는 side_effect 에 예외를 줍니다.

Python
>>> mock = Mock(side_effect=KeyError('foo'))
>>> mock()
Traceback (most recent call last):
 ...
KeyError: 'foo'

가짜를 부르는 순간 정해 둔 KeyError 가 던져집니다. side_effect 에는 함수나 목록도 줄 수 있습니다.

Python
>>> values = {'a': 1, 'b': 2, 'c': 3}
>>> def side_effect(arg):
...     return values[arg]
...
>>> mock.side_effect = side_effect
>>> mock('a'), mock('b'), mock('c')
(1, 2, 3)
>>> mock.side_effect = [5, 4, 3, 2, 1]
>>> mock(), mock(), mock()
(5, 4, 3)

side_effect 에 함수를 주면 인자마다 다른 답을 냅니다. 'a' 에는 1, 'b' 에는 2 입니다. 목록을 주면 부를 때마다 다음 값을 꺼내 돌려줍니다.

Jest

자바스크립트 테스트 도구입니다. jest.fn() 으로 가짜 함수를 만듭니다. mockReturnValueOnce 로는 한 번씩 쓸 답을 줄 세웁니다.

JavaScript
const myMock = jest.fn();

console.log(myMock());
// > undefined
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

답을 정하기 전의 가짜는 undefined 를 돌려줍니다. 답을 정한 뒤에는 첫 호출이 10, 둘째가 'x' 입니다. 줄 세운 답이 떨어지면 mockReturnValue(true) 로 정한 true 가 이어집니다.

RSpec

루비 테스트 도구입니다. 루비에서는 메서드를 부르는 일을 객체에 메시지를 보낸다고 말합니다. allow(...).to receive(...) 는 가짜가 어떤 메시지를 받아도 되는지 정합니다. 답은 and_return 으로 정합니다.

Ruby
dbl = double
allow(dbl).to receive(:foo).and_return(14)
expect(dbl.foo).to eq(14)

foo 메시지를 받은 가짜는 14 를 돌려줍니다. and_return 에 값을 여럿 주면 부를 때마다 차례로 돌려줍니다.

Ruby
dbl = double
allow(dbl).to receive(:foo).and_return(1, 2, 3)

expect(dbl.foo).to eq(1)
expect(dbl.foo).to eq(2)
expect(dbl.foo).to eq(3)
expect(dbl.foo).to eq(3) # begins to repeat last value
expect(dbl.foo).to eq(3)

값이 떨어지면 마지막 값 3 을 계속 돌려줍니다. 답 없이 허락만 한 메시지는 nil 을 돌려줍니다. 예외는 and_raise 로 정합니다.

Ruby
dbl = double
allow(dbl).to receive(:foo).and_raise("boom")
dbl.foo

and_raise("boom") 을 정한 가짜를 부르면 RuntimeError: boom 이 납니다. RSpec 의 가짜는 기본이 엄격합니다. 허락하지 않은 메시지를 받으면 오류를 냅니다.

도구별 답의 모양

상세에서 본 네 모양이 각 도구에서 어떻게 적히는지 모으면 이렇습니다.

답의 모양 Mockito unittest.mock Jest RSpec
늘 같은 값 thenReturn("first") return_value=3 mockReturnValue(true) and_return(14)
입력마다 다른 값 인자마다 when 을 따로 side_effect 에 함수 — —
예외 thenThrow(...) side_effect=KeyError('foo') — and_raise("boom")
부를 때마다 다른 값 thenThrow(...).thenReturn("foo") side_effect=[5, 4, 3, 2, 1] mockReturnValueOnce(10) 이어 붙이기 and_return(1, 2, 3)

줄표(—)는 이 편이 보인 예제에 그 꼴이 없다는 뜻입니다. 도구가 그 일을 못 한다는 뜻은 아닙니다.

경계

목 객체는 자기가 받은 호출을 기억해 두는 테스트 더블입니다. 테스트는 그 기록을 보고 테스트 대상 코드가 협력 객체를 제대로 불렀는지 확인합니다.

그러면 목 객체에 반환값을 정하는 것도 스터빙인가요? 맞습니다. 스터빙은 테스트 더블의 종류가 아니라 함수에 답을 정해 주는 행위의 이름입니다. 그 테스트 더블이 스텁이든 목이든 답을 정하면 스터빙입니다.

목 객체로 호출을 확인하는 일은 호출 검증이라고 합니다. 함수가 불렸는지, 몇 번 불렸는지, 맞는 인자로 불렸는지를 봅니다.

같은 가짜 객체로 두 일을 다 할 수 있습니다. 권한 서비스 예를 Mockito 로 적으면 이렇습니다. 먼저 스터빙입니다.

Java
// The user ID shouldn't have access if null is returned.
when(mockAuthorizationService.lookupUser(USER_ID)).thenReturn(null);
assertThat(accessManager.userHasAccess(USER_ID)).isFalse();

lookupUser 가 null 을 돌려주게 정했습니다. 이것이 스터빙입니다. 다음은 같은 가짜로 한 호출 검증입니다.

Java
// The test will fail if accessManager.userHasAccess(USER_ID) didn't call
// mockAuthorizationService.lookupUser(USER_ID).
verify(mockAuthorizationService).lookupUser(USER_ID);

lookupUser 가 불렸는지 확인합니다. 이것이 호출 검증입니다. 두 코드 모두 모킹 프레임워크가 만든 같은 가짜 mockAuthorizationService 를 씁니다.

스터빙 호출 검증
다루는 것 협력 객체가 돌려주는 값 협력 객체가 받은 호출
방향 테스트 대상 코드로 들어간다 테스트 대상 코드에서 나온다
무엇으로 판정하나 테스트 대상 코드가 낸 결과 호출이 있었나 · 인자가 맞나

헷갈리는 까닭은 이름에 있습니다. 호출 검증을 「모킹」이라고 부르기도 합니다. 그런데 모킹 프레임워크는 스터빙에도 호출 검증에도 쓰일 수 있습니다. 「목을 썼다」만으로는 어느 쪽 일을 했는지 갈리지 않습니다. 가르는 기준은 물건이 아니라 테스트가 그 가짜로 무엇을 했나입니다.

대가

스터빙은 쉽게 걸 수 있어서, 진짜 구현을 쓰기 번거로울 때마다 손이 가기 쉽습니다. 그렇게 과하게 쓰면 그 테스트를 관리하는 개발자의 생산성이 크게 떨어질 수 있습니다. 치르는 값은 셋입니다.

테스트가 흐려진다

스터빙은 가짜의 동작을 정하는 코드를 테스트에 더 씁니다. 이 코드가 늘어날수록 테스트가 무엇을 확인하려는지가 묻힙니다. 테스트 대상 코드의 속을 모르는 사람은 이 준비 코드를 읽기 어려울 수 있습니다.

신호가 하나 있습니다. 왜 이 함수를 스터빙했는지 알려고 테스트 대상 코드를 머릿속으로 한 줄씩 따라가고 있다면, 그 테스트에는 스터빙이 맞지 않는 것입니다.

테스트가 깨지기 쉬워진다

스터빙은 테스트 대상 코드의 구현 세부를 테스트로 새어 나오게 합니다. 어느 협력 객체의 어느 메서드를 부르는지를 테스트가 알아야 답을 정할 수 있기 때문입니다. 구현 세부가 바뀌면 테스트도 고쳐야 합니다.

테스트는 사용자에게 보이는 동작이 바뀔 때만 고치게 되는 것이 이상적입니다. 구현을 바꾸는 것만으로는 테스트가 영향을 받지 않아야 합니다. 스터빙이 쌓인 테스트는 이 이상에서 멀어집니다.

테스트가 덜 미덥다

함수가 어떤 입력에 어떤 결과를 내기로 한 약속을 계약이라고 합니다. 「1 과 2 를 넣으면 3 을 돌려준다」는 덧셈 함수 add() 의 계약 한 조각입니다. 이것을 스텁으로 박아 두어도 스텁이 진짜와 같게 동작한다는 보장은 없습니다.

Java
when(stubCalculator.add(1, 2)).thenReturn(3);

테스트 대상 코드가 진짜 구현의 계약에 기대고 있으면 스터빙이 맞지 않습니다. 계약의 세부를 테스트에 베껴 적어야 하기 때문입니다. 그 베낀 계약이 맞는지는 확인할 길이 없습니다.

스텁은 상태를 저장하지도 못합니다. 앞선 호출이 남긴 것을 다음 호출에서 꺼낼 수 없다는 뜻입니다.

이 대목에서 견줄 것이 페이크입니다. 페이크는 진짜와 비슷하게 동작하지만 운영에는 못 쓰는 간단한 구현입니다. 메모리 안에서만 도는 데이터베이스가 그 예입니다.

진짜 데이터베이스나 페이크라면 database.save(item) 으로 저장한 것을 database.get(item.id()) 로 다시 꺼낼 수도 있습니다. 두 호출이 같은 내부 상태를 보기 때문입니다. 스텁으로는 이렇게 할 길이 없습니다.

가짜를 끼운 테스트는 운영과 다른 구성으로 도는 셈이기도 합니다. 그래서 테스트 더블 없이 도는지 확인하는 테스트를 적어도 하나는 두어야 합니다.

스터빙을 과하게 쓴 테스트

아래 테스트는 결제 처리 코드를 시험하려고 협력 객체 둘의 메서드 다섯을 스터빙했습니다.

Java
@Test public void creditCardIsCharged() {
  // Pass in test doubles that were created by a mocking framework.
  paymentProcessor =
      new PaymentProcessor(mockCreditCardServer, mockTransactionProcessor);
  // Set up stubbing for these test doubles.
  when(mockCreditCardServer.isServerAvailable()).thenReturn(true);
  when(mockTransactionProcessor.beginTransaction()).thenReturn(transaction);
  when(mockCreditCardServer.initTransaction(transaction)).thenReturn(true);
  when(mockCreditCardServer.pay(transaction, creditCard, 500))
      .thenReturn(false);
  when(mockTransactionProcessor.endTransaction()).thenReturn(true);
  // Call the system under test.
  paymentProcessor.processPayment(creditCard, Money.dollars(500));
  // There is no way to tell if the pay() method actually carried out the
  // transaction, so the only thing the test can do is verify that the
  // pay() method was called.
  verify(mockCreditCardServer).pay(transaction, creditCard, 500);
}

준비 코드가 테스트의 대부분을 차지합니다. 결제가 정말 됐는지는 알 수 없어서 pay() 가 불렸는지만 확인합니다. 같은 테스트를 신용카드 서버의 페이크로 다시 쓰면 준비 코드가 사라집니다. 그 페이크에 마지막 결제 금액을 물어 500 인지 확인하면 됩니다.

쓰기에 알맞을 때

스터빙은 진짜 구현을 통째로 갈아 끼우는 수단이 아닙니다. 테스트 대상 코드를 어떤 상태로 몰아넣으려고 함수 하나가 특정 값을 돌려줘야 할 때 알맞습니다. 거래 수를 세는 코드를 시험하려면 거래 조회가 비어 있지 않은 목록을 돌려줘야 하는 경우가 그렇습니다.

답을 테스트 안에서 바로 정하므로, 진짜 구현이나 페이크로는 일으키기 어려운 반환값과 오류도 흉내 낼 수 있습니다.

스터빙한 함수 하나하나가 테스트의 확인 문장과 곧바로 이어져야 합니다. 그래야 왜 그 함수를 스터빙했는지 드러납니다. 한 테스트는 대개 함수 몇 개만 스터빙해야 합니다.

스터빙할 함수가 많아야 하는 테스트는 스터빙을 과하게 쓰고 있다는 신호일 수 있습니다. 테스트 대상 코드가 너무 복잡해 나눠야 한다는 신호일 수도 있습니다.

스터빙이 알맞을 때도 진짜 구현이나 페이크가 여전히 먼저입니다. 둘은 구현 세부를 드러내지 않습니다. 코드가 맞다는 보장도 스터빙보다 더 많이 줍니다. 다만 테스트가 지나치게 복잡해지지 않게 쓰임을 묶어 두면 스터빙도 합리적인 기법일 수 있습니다.

아래 그림은 이 절의 판단을 한데 모은 것입니다.

flowchart TD
    시작["테스트 대상 코드를 어떤 상태로 몰고 싶다"] --> 물음1{"진짜 구현이나 페이크로<br/>그 상태를 만들 수 있나"}
    물음1 -->|예| 진짜["진짜 구현이나 페이크를 쓴다"]
    물음1 -->|아니오| 물음2{"함수 몇 개의 반환값이나<br/>오류만 정하면 되나"}
    물음2 -->|예| 스터빙["그 함수들을 스터빙한다"]
    물음2 -->|아니오| 신호["과용 신호일 수 있다<br/>테스트 대상 코드를 나눌 신호일 수도 있다"]

관련 항목

스터빙으로 만드는 테스트 더블

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

스터빙을 대신 해 주는 도구

모킹 프레임워크 · Mockito · unittest.mock · Jest · RSpec · 몽키 패칭 · 동적 프록시

스터빙이 쓰이는 테스트 종류와 개발 방식

단위 테스트 · 통합 테스트 · 계약 테스트 · 테스트 픽스처 · 테스트 주도 개발 · QA와 테스트

스터빙과 맞세워지는 검증 방식

호출 검증 · 상태 검증 · 행위 검증 · 명령-조회 분리

스터빙이 기대는 설계 수단

의존성 주입 · 인터페이스 · 람다 표현식 · 생성자 주입 · 의존성 역전 원칙

스터빙으로 바꿔 끼우는 협력 대상

데이터베이스 · 외부 API · 타임아웃 · 예외 · 시계 추상화

스터빙이 막거나 부르는 테스트 문제

불안정한 테스트 · 깨지기 쉬운 테스트 · 과도한 모킹 · 변경 감지 테스트 · 테스트 격리

스터빙 도구를 쓰는 프로그래밍 언어

Java · Python · JavaScript · 루비

다른 이름: stubbing · 스텁 설정 · 스텁하기