RSpec에서 모크(Mock)란 무엇인가?
모크는 RSpec에만 있는 개념이 아니라, 테스트 분야 전반에서 통용되는 개념입니다.
모크란 테스트에 사용되는 객체입니다.
일반적인 기대(expectation)처럼 출력 값을 검증하는 대신, 모크는 두 객체 간의 상호작용을 테스트할 때 사용합니다.
예를 들어, 이미지를 뒤집는 API를 작성한다고 가정해 봅시다.
직접 이미지 조작 코드를 작성하는 대신 mini_magick 같은 젬(gem)을 사용하게 되죠.
그렇다면 여러분의 코드와 이 외부 의존성 사이의 상호작용을 테스트하고 싶어질 겁니다. 이때 ImageProcessor 클래스에 올바른 메서드가 호출되는지 기대하는 모크를 작성하면 됩니다.
이렇게 하면 테스트를 실행할 때마다 실제로 이미지를 뒤집는(느린 작업) 일이 없어집니다.
어떻게 그럴 수 있을까요?
모크는 원본 객체를 대체하기 때문에 실제 메서드는 호출되지 않습니다.
이제 코드 예제를 살펴볼 시간입니다!
RSpec 모크 예제
다음은 ImageFlipper 테스트입니다:
RSpec.describe "ImageFlipper" do
it "calls the flip method with the correct arguments" do
mock = double("mini_magick")
expect(mock).to receive(:flip).with("ruby.jpg")
img = ImageFlipper.new(mock)
img.flip("ruby.jpg")
end
end
이 테스트를 통해 TDD 방식으로 코드를 작성할 수 있습니다.
먼저 ImageFlipper 클래스를 작성해야 합니다.
다음과 같이 말이죠:
class ImageFlipper
def initialize(image_processor)
@image_processor = image_processor
end
end
flip 메서드도 필요합니다:
def flip(file_name) end
이제 RSpec에서 다음과 같은 피드백을 받게 됩니다:
Failures:
1) ImageFlipper calls the flip method with the correct arguments
Failure/Error: expect(mock).to receive(:flip).with("ruby.jpg")
(Double "mini_magick").flip("ruby.jpg")
expected: 1 time with arguments: ("ruby.jpg")
received: 0 times
# ./rspec-mocks.rb:6:in `block (2 levels) in <top (required)>'
이 결과는 flip 메서드가 0번 호출되었지만, 1번 호출될 것으로 기대했다는 의미입니다.
테스트가 원하는 것을 만들어 주면 테스트를 통과시킬 수 있습니다:
def flip(file_name) @image_processor.flip(file_name) end
자, 이제 테스트가 통과했습니다:
. Finished in 0.00751 seconds (files took 0.11157 seconds to load) 1 example, 0 failures
정리: 우리가 한 일은 무엇일까요?
image_processor를 인자로 받는 ImageFlipper 클래스를 만들었습니다.
이 프로세서는 flip 메서드에 응답합니다.
그리고 모크를 사용해 이 메서드가 특정 인자와 함께 딱 한 번 호출되었는지 검증했습니다.
네, 알고 있습니다. 이 예제는 아주 간단합니다.
하지만 파일 존재 여부 확인, 유효한 이미지인지 검사 등을 포함한 완전한 ImageFlipper 구현을 충분히 상상해 볼 수 있습니다.
모크와 값 테스트의 차이
일반적인 테스트에서는 메서드의 반환 값을 검증합니다:
"이 메서드가 뒤집힌 이미지를 반환했나요?"
반면 모크를 사용할 때는 행위(behavior)를 테스트합니다:
"우리는 다른 객체에게 올바른 정보로, 필요한 만큼 정확히 올바른 지시를 했나요?"
모크(Mock) vs 스텁(Stub)
혼동하기 쉬운 또 하나의 지점은 모크와 스텁을 비교하는 것입니다.
차이점은 무엇일까요?
- 스텁은 미리 준비된 응답(canned response)을 반환하는 메서드일 뿐이며, 행위는 전혀 신경 쓰지 않습니다.
- 모크는 메서드가 호출되기를 기대하며, 호출되지 않으면 테스트가 실패합니다.
RSpec에서 스텁은 다음과 같이 작성합니다:
stub = double("json")
allow(stub).to receive(:response) do
{"blog"=>"rubyguides.com", "rating"=>"5/5"}.to_json
end
allow 메서드를 사용했기 때문에 이것이 스텁입니다.
테스트 객체인 double("json")이 이 메서드를 받아 응답하도록 허용할 뿐, 호출 여부는 검증하지 않습니다.
바로 이것이 차이입니다!
검증된 더블(Verified Doubles) 사용 방법
모크와 스텁의 단점 중 하나는, 프로덕션 코드에 존재하지 않는 메서드를 사용하게 될 수 있다는 점입니다.
메서드 이름이 변경되었거나... 오타를 냈을 수도 있으니까요!
이 문제를 해결해 주는 것이 바로 검증된 더블(verified doubles)입니다.
검증된 더블은 스텁(allow) 또는 모크(expect) 어느 쪽으로든 사용할 수 있으며, 해당 이름의 메서드가 실제로 존재하는지 확인해 줍니다.
예제:
mock = instance_double(ImageProcessor)
메서드가 존재하지 않으면 에러가 발생합니다:
1) ImageFlipper calls the flip method with the correct arguments
Failure/Error: expect(mock).to receive(:flip).with("ruby.jpg")
the ImageProcessor class does not implement the instance method: flip
메서드가 존재한다면 정상적으로 동작합니다.
값을 반환하는 모크
다시 모크 이야기로 돌아가 보겠습니다.
앞선 예제에서 우리는 이런 코드를 사용했습니다:
expect(mock).to receive(:flip).with("ruby.jpg")
여러분의 코드가 flip을 호출하면 모크는 nil을 반환합니다.
만약 코드가 nil이 아닌 다른 값을 기대하고 있다면 에러가 발생합니다.
이 경우 모크가 결과값을 반환하도록 만들면 해결됩니다.
다음과 같이 말이죠:
expect(mock).to receive(:flip).with("ruby.jpg").and_return("ruby-flipped.jpg")
인스턴스 메서드 모킹 방법
다음과 같은 코드가 있다고 가정해 봅시다:
class NumberGenerator
def random
"A" * rand(1..10)
end
end
무작위성 때문에 이 메서드는 테스트하기 어렵습니다.
RSpec에서는 rand를 모크하거나 스텁할 수 있습니다.
다음과 같이 말이죠:
it "generates a random number" do
generator = NumberGenerator.new
allow(generator).to receive(:rand).and_return(5)
expect(generator.random).to eq("AAAAA")
end
이제 rand는 고정된 값을 반환하므로, 메서드의 결과를 테스트하는 데 활용할 수 있습니다.
이상적으로는 의존성(여기서는 rand 함수)을 주입해서 직접 제어할 수 있게 하는 것이 좋습니다. 의존성 주입이란 파라미터로 전달하는 것을 의미하며, 거창한 기술은 아닙니다.
하지만 때로는 그냥 메서드를 스텁하는 편이 더 편리할 수 있습니다.
언제 모크를 사용해야 할까?
자, 이제 중요한 질문입니다...
정확히 언제 모크를 사용해야 할까요?
소프트웨어 개발은 복잡한 주제입니다.
하지만 따라 할 수 있는 몇 가지 가이드라인이 있습니다:
- 테스트 대상 메서드가 값을 반환하고 부작용(파일 생성, API 요청 등)이 없다면 모크가 필요 없습니다. 그냥 반환 값만 검증하세요.
- 메서드가 외부 객체와 함께 작동하며 그들에게 명령을 내린다면, 이러한 객체들과의 상호작용을 모크할 수 있습니다.
- 메서드가 외부 서비스(예: API)에서 데이터를 요청한다면, 테스트 목적으로 이 데이터를 제공하는 스텁을 사용할 수 있습니다.
모킹은 외부 세계와의 상호작용에만 사용하는 것이 좋습니다.
즉...
자신의 애플리케이션 클래스를 모킹하는 것은 피하세요!
왜냐하면 테스트가 구현 세부사항과 결합되어 코드 변경이 더 어려워지기 때문입니다.
유일한 예외는 서드 파티 코드의 래퍼(wrapper) 역할을 하는 클래스입니다.
더 자세히 알고 싶다면 관련 영상을 참고하세요.
마치며
RSpec의 모크, 스텁, 검증된 더블에 대해 배워 보았습니다!
더 많은 사람들이 이 콘텐츠를 즐기고 도움을 받을 수 있도록 글을 공유해 주세요.
읽어주셔서 감사합니다 🙂