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

VCR 젬으로 Ruby 테스트 스위트 개선하기: WebMock과 함께하는 실전 가이드

Ruby 애플리케이션이 외부 API를 사용하고 있다면, 아마 느린 테스트와 API 호출 제한(Rate Limit) 문제를 겪어본 적이 있을 것입니다.

해결책은 무엇일까요?

클라이언트 라이브러리의 HTTP 메서드를 직접 스텁(stub) 처리하고, 미리 정해둔 응답을 반환하도록 만들 수도 있습니다.

하지만 이 방식은 작업량이 많고 코드도 지저분해지기 쉽습니다!

더 나은 방법은 WebMock + VCR처럼 강력한 젬(gem) 조합을 활용하는 것입니다.

WebMock은 다음과 같은 주요 HTTP 라이브러리에서 발생하는 HTTP 요청을 가로챕니다:

  • net/http
  • Faraday
  • RestClient
  • …그 외에도 수많은 라이브러리!

이 기능만으로도 유용하지만, 여전히 응답 데이터를 직접 준비해야 하는 불편함이 남습니다.

바로 여기서 VCR이 등장합니다…

VCR은 WebMock과 함께 작동하여 코드가 보낸 HTTP 요청에 대한 응답을 기록합니다.

이렇게 기록된 파일들을 "카세트(cassette)"라고 부릅니다.

테스트를 실행하면:

VCR은 카세트 파일을 불러와 기록해 둔 응답을 그대로 반환합니다. 실제 API에 요청할 필요가 없으므로 응답 속도가 훨씬 빨라집니다.

그럼 지금부터 코드 예제를 살펴보겠습니다!

VCR 코드 예제

이 예제에서는 RSpec을 사용합니다. 다음 섹션에서 확인하게 되겠지만, RSpec이 VCR과 더 잘 통합되기 때문입니다.

테스트하려는 코드는 다음과 같습니다:

require "faraday"
require "json"

class Github
  def self.user(name)
    url  = "https://api.github.com/users/#{name}"
    data = Faraday.get(url).body

    JSON.parse(data, symbolize_names: true)
  end
end

GitHub API에 요청을 보내 특정 사용자의 정보를 가져오는 간단한 코드입니다. 단순하지만 VCR의 작동 원리를 익히기에는 충분합니다.

이 코드에 대한 테스트는 이렇게 작성할 수 있습니다:

require "rspec/autorun"

require_relative "github_api_example"

describe Github do
  let(:user_response) { Github.user("ruby") }

  it "can fetch & parse user data" do
    expect(user_response).to be_kind_of(Hash)

    expect(user_response).to have_key(:id)
    expect(user_response).to have_key(:type)
  end
end

이 테스트는 실제 API를 호출하며 통과하지만, 실행에 약 0.5초가 걸립니다.

단 하나의 테스트에 0.5초!

크게 느껴지지 않을 수 있지만, 테스트가 100개라면 전체 실행에 50초가 걸리는 셈입니다.

이제 이 문제를 해결해 볼까요?

다음 코드를 추가하여 VCR을 도입해 보겠습니다:

require "vcr"

VCR.configure do |c|
  c.cassette_library_dir = "spec/vcr"
  c.hook_into :webmock
end

이 코드는 test_helper 파일에 추가하면 모든 테스트에서 사용할 수 있습니다.

configure 블록을 통해 VCR에게 카세트 파일을 저장할 위치를 알려주고, WebMock 통합을 활성화합니다.

Faraday나 Excon을 사용 중이라면 VCR이 해당 라이브러리에 직접 연결(hook)될 수도 있습니다.

이 경우 :webmock 대신 :faraday 또는 :excon으로 바꿔주기만 하면 됩니다.

다음 단계:

VCR에게 카세트 이름과 어떤 코드가 그 안에서 실행될지 알려줘야 합니다.

방법은 다음과 같습니다:

let(:user_response) do
  VCR.use_cassette("github/user") { Github.user("ruby") }
end

테스트를 실행하면 VCR이 cassette_library_dir 아래에 파일을 생성합니다. 이 경우 파일명은 spec/vcr/github/user.yaml이 됩니다. 직접 따라 해보신다면 생성된 파일을 한번 열어보는 것도 좋습니다.

이제 테스트를 실행하면 훨씬 빨라진 것을 확인할 수 있습니다.

실제로…

완료까지 단 0.01초밖에 걸리지 않습니다!

"An HTTP request has been made that VCR does not know how to handle" 오류 해결하기

이 오류 메시지를 만났다면 두 가지 원인 중 하나일 가능성이 높습니다.

1. VCR이 활성화된 상태에서 VCR.use_cassette 블록 밖에서 HTTP 호출을 시도하는 경우.

해결책: 기본적으로 VCR + WebMock은 모든 HTTP 요청을 차단합니다. 설정 옵션으로 이 동작을 변경하거나, 누락된 VCR 블록을 추가하거나, RSpec 메타데이터(다음 섹션 참고)를 활용하면 됩니다.

2. 다른 요청을 보내면서 해당 URL과 일치하지 않는 카세트를 사용하는 경우. 예를 들어 테스트에서 /users/ruby를 요청하면 카세트가 이 URL에 맞게 생성됩니다. 테스트를 /users/apple로 변경하면 카세트가 다른 URL용이므로 이 오류가 발생합니다.

해결책: URL마다 서로 다른 카세트를 사용하거나, new_episodes 녹화 모드를 활성화(vcr: { record: :new_episodes })하거나, 요청 URL을 변경한 후 기존 카세트를 삭제하세요.

RSpec 메타데이터로 카세트 자동 생성하기

VCR.use_cassette 메서드는 이 젬을 활용하는 좋은 방법입니다.

하지만…

VCR이 자동으로 카세트를 생성하도록 설정할 수도 있습니다.

어떻게 할까요?

VCR.configure 블록 안에 다음 한 줄을 추가합니다:

c.configure_rspec_metadata!

이제 특정 테스트(it 블록) 또는 테스트 그룹(describe)에 대해 VCR을 선택적으로 활성화할 수 있습니다.

다음과 같이 말이죠:

describe Github, :vcr do
  # ...
end

이렇게 하면 테스트 설명을 기반으로 카세트 파일이 자동 생성됩니다:

spec/vcr/
└── Github
    ├── can_parse_user_data.yml
    └── can_test_vcr.yml

참고로 이 방식은 테스트당 하나의 카세트를 생성합니다…

두 테스트가 동일한 요청을 보내고 같은 데이터를 사용하더라도, VCR은 각각 별도의 카세트를 만듭니다.

알아두면 유용한 VCR 옵션과 팁

카세트에 문제가 있거나 최신 데이터가 필요하다면 카세트 파일을 삭제하면 됩니다.

VCR이 새로운 API 응답을 다시 기록하므로 문제가 해결될 수 있습니다.

만약 그래도 해결되지 않는다면 어떻게 할까요?

VCR.configure에서 디버그 모드를 활성화할 수 있습니다:

VCR.configure do |c|
  # ...
  c.debug_logger = $stderr
end

출력량이 상당히 많을 수 있으니, 관심 있는 테스트만 골라서 실행하는 것이 좋습니다.

다음으로:

API 응답에 API 키나 기타 민감한 정보가 포함되어 있다면…

녹음 내용에서 해당 데이터를 필터링하고 싶을 것입니다.

방법은 다음과 같습니다:

VCR.configure do |c|
  # ...
  c.define_cassette_placeholder("<API_KEY>", ENV["API_KEY"])
end

마치며

이번 글에서는 WebMock과 VCR 젬을 활용해 외부 API 응답을 기다릴 필요 없이 Ruby 애플리케이션의 테스트를 작성하는 방법을 알아보았습니다!

VCR 젬으로 Ruby 테스트 스위트 개선하기: WebMock과 함께하는 실전 가이드

이 글이 도움이 되었다면, 더 많은 분들이 볼 수 있도록 공유해 주시는 것도 잊지 마세요.

읽어주셔서 감사합니다 🙂