Home
All Posts
🕵🏻

테스트 더블 Mock vs. Stub vs. Spy

Created
2023/11/27
Tags
Test
Category
Knowledge
2 more properties

테스트 더블

일단 요약하자면 Mock, Stub, Spy 각각은 테스트의 목적과 요구 사항에 따라 다르게 사용하고, 종종 함께 사용되기도 한다. 테스트 코드를 작성하다 보면 위 세 가지 개념을 마주하게 되는데 짚고 넘어가 보자.
Mock, Stub, Spy는 소프트웨어 테스트에서 자주 사용되는 용어로, 모두 테스트 더블(test double)의 일종이다. 테스트 더블이란, xUnit Test Patterns의 저자인 제라드 메스자로스(Gerard Meszaros)가 만든 용어로 테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체를 말한다. (참고: Test Double을 알아보자)
테스트 더블 (Test Double)
각각은 테스트 중에 외부 시스템이나 복잡한 컴포넌트를 대체하는 데에 사용되지만, 그 목적과 사용 방법이 다르다. 아래에 이들의 차이점을 정리해 보았다.

Mock

기대한 대로 잘 호출되나?
Mock은 반환값이 없는 함수를 테스트할 때, 특정 객체에서 특정 함수가 호출되었는지 테스트할 때 사용한다. 다시 말하면, 내가 바라는 호출에 대한 기대를 명시해 두고, 명시한 내용에 따라 잘 작동하는지 프로그래밍된 객체인 거다.
Mock은 테스트 중에 특정 인터페이스를 구현하는 객체를 대체하는 데 사용된다. 즉, 가짜 테스트 수단을 만들어 두는 것이다. 이렇게 대체된 Mock 객체는 위에 언급한 대로 특정 메서드가 호출되었는지, 어떤 인자로 호출되었는지 검사하는 데 중점을 둔다.
Mock은 테스트 대상의 외부 의존성을 대체하고, 그 객체의 행동을 시뮬레이션한다. 예를 들어, 데이터 베이스로부터 조회한 값을 연산하는 로직을 구현하는 상황이라고 하자. 이 로직을 테스트하기 위해서는 어쩔 수 없이 DB의 영향을 받을 것이므로 DB 상태에 따라 매번 다른 결과를 낼 수 있기에 테스트를 하기에는 불안정한 상황이다. 불안정하기에 안정적인 대상으로 대체하는 것이다.
아래 테스트 코드 일부를 보며 mock을 이해해 보자.
test('게임 종료 후 재시작', async () => { // ... 생략 mockRandoms(randoms); mockQuestions(answers); // App 인스턴스 생성 및 play 함수 실행const app = new App(); await expect(app.play()).resolves.not.toThrow(); // 로그 출력이 예상대로 이루어졌는지 검증 messages.forEach((output) => { expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output)); }); });
JavaScript
복사
위 예제 코드를 보면 Mock 기능을 사용하여 실제 함수 호출을 추적하고, 해당 함수가 특정 조건에 맞게 호출되었는지 검증한다. 덕분에 특정 상황에서 적절한 로그가 출력되는지 확인할 수 있다.

Stub

기대한 값이 잘 반환되나?
Stub은 특정 객체에서 특정 함수를 호출할 때, 특정한 값이 반환되기를 기대하며 더미 데이터를 지정할 때 사용한다. 'stub'의 사전적 의미를 미루어 보아, 어떤 찌꺼기 같은 것을 준비해 두어, 기대한 값을 테스트하는 것이라고 이해할 수 있을 것 같다.
stub의 사전적 의미 @naverdic
구체적으로 풀어 말하자면, stub은 dummy 객체가 실제로 동작하는 것처럼 꾸며 놓은 객체로, 테스트에서 호출된 요청에 대해 미리 준비해 둔 결과를 제공한다. 따라서 내가 기대한 값이 잘 반환되는지 결과를 확인할 수 있다.
Stub은 테스트 중에 사용되는 하드코딩된 값 또는 응답을 제공하는 데 사용된다. 복잡한 로직이나 외부 시스템과의 상호작용을 단순화하고, 테스트를 더 예측 가능하게 만드는 데 중점을 두며, 인터페이스 또는 기본 클래스가 최소한으로 구현된 상태다.
아래 테스트 코드 일부를 보며 stub을 이해해 보자.
test.each([ [['pobi,javaji']], [['pobi,eastjun']] ])('이름에 대한 예외 처리', async (inputs) => { // given: 질문 모의를 설정 mockQuestions(inputs); // when: App 인스턴스를 생성 const app = new App(); // then: App의 play 메소드가 비동기로 실행될 때 예외 '[ERROR]'를 던지는지 확인 await expect(app.play()).rejects.toThrow('[ERROR]'); });
JavaScript
복사
위 코드에[['pobi,javaji']], [['pobi,eastjun']]는 테스트에서 사용하는 stub으로서, 실제 사용자 입력을 대신해 테스트 환경에서 App 클래스의 동작을 시뮬레이션하는 데 사용된다.
참고로 test.each 메서드는 이러한 stub들을 다루는 데에 빈번하게 사용되는데, 각각의 배열이 다른 테스트 케이스로 사용된다. 이를 통해 다양한 입력 시나리오에 대한 동작에 대한 코드의 반복을 줄이면서 효율적으로 검증할 수 있다.

Spy

함수가 잘 사용되고 있나?
Spy는 특정 함수만 실제 함수를 호출하게 하고 싶을 때, 기존의 객체나 함수를 감시하는 데에 사용된다. 이때 함수 호출, 전달된 인자, 반환값 등을 기록하지만, 실제 로직의 동작을 변경하지는 않는다.
풀어 설명하자면, Spy는 주로 테스트 중에 특정 함수나 메서드가 어떻게 사용되는지 관찰하고 기록하는 데 사용된다. 마치 검은색 옷을 입은 스파이가 특정 함수가 호출되었을 때 확인이 필요한 부분을 기록하고 있는 모습을 상상하면 된다.
예를 들어, 함수가 몇 번 호출되었는지, 어떤 인자로 호출되었는지 등을 검사할 수 있다. 실제 구현을 유지하면서도 함수의 사용에 대한 상세한 정보를 제공하는 것이 특징이다.
아래 테스트 코드 일부를 보며 spy를 이해해 보자.
// 로그 출력을 감시하는 Spy 함수를 생성 const getLogSpy = () => { // MissionUtils의 Console.print를 감시 const logSpy = jest.spyOn(MissionUtils.Console, 'print'); // 이전의 감시 기록을 초기화 logSpy.mockClear(); // 생성된 Spy 객체를 반환return logSpy; };
JavaScript
복사
위 예시 코드에서는 jest.spyOn을 사용하여 MissionUtils.Console.print 함수에 대한 spy를 생성하며, 이 spy는 print 함수의 실제 호출을 감시하고 기록한다. 함수가 호출되었을 때의 인자, 호출 횟수 등을 추적할 수 있고, 앞서 설명한 것처럼 실제 함수의 로직은 그대로 유지된다.

결론

앞으로 테스트 코드를 작성할 때는 위의 프레임워크들이 제공해 주는 기능을 적재적소에 사용하며 제대로 된 테스트 코드를 작성해 보자. 어떤 상황에 어떤 것을 써야 하는지 학습하고 사용한다면 더 좋은 테스트 코드를 작성할 수 있지 않을까.

참고 자료