Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

RSpec 모크(Mock) 사용 방법: 단계별 완벽 가이드

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 함수)을 주입해서 직접 제어할 수 있게 하는 것이 좋습니다. 의존성 주입이란 파라미터로 전달하는 것을 의미하며, 거창한 기술은 아닙니다.

하지만 때로는 그냥 메서드를 스텁하는 편이 더 편리할 수 있습니다.

언제 모크를 사용해야 할까?

자, 이제 중요한 질문입니다...

정확히 언제 모크를 사용해야 할까요?

소프트웨어 개발은 복잡한 주제입니다.

하지만 따라 할 수 있는 몇 가지 가이드라인이 있습니다:

  1. 테스트 대상 메서드가 값을 반환하고 부작용(파일 생성, API 요청 등)이 없다면 모크가 필요 없습니다. 그냥 반환 값만 검증하세요.
  2. 메서드가 외부 객체와 함께 작동하며 그들에게 명령을 내린다면, 이러한 객체들과의 상호작용을 모크할 수 있습니다.
  3. 메서드가 외부 서비스(예: API)에서 데이터를 요청한다면, 테스트 목적으로 이 데이터를 제공하는 스텁을 사용할 수 있습니다.

모킹은 외부 세계와의 상호작용에만 사용하는 것이 좋습니다.

즉...

자신의 애플리케이션 클래스를 모킹하는 것은 피하세요!

왜냐하면 테스트가 구현 세부사항과 결합되어 코드 변경이 더 어려워지기 때문입니다.

유일한 예외는 서드 파티 코드의 래퍼(wrapper) 역할을 하는 클래스입니다.

더 자세히 알고 싶다면 관련 영상을 참고하세요.

마치며

RSpec의 모크, 스텁, 검증된 더블에 대해 배워 보았습니다!

더 많은 사람들이 이 콘텐츠를 즐기고 도움을 받을 수 있도록 글을 공유해 주세요.

읽어주셔서 감사합니다 🙂