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

Ruby에서 외부 서비스 테스트하기, 생각보다 쉽습니다

새 프로젝트를 시작했고, 이제 코드가 서드파티 서비스에 의존할 차례입니다. ElasticSearch나 Resque 같은 도구일 수도 있고, 결제 제공업체 또는 임의의 HTTP API일 수도 있습니다. 좋은 개발자라면 이런 코드도 철저히 테스트하고 싶어질 겁니다. 하지만 내가 전혀 통제할 수 없는 서비스로 요청을 보내는 코드는 대체 어떻게 테스트해야 할까요?

테스트를 건너뛸 수도 있지만, 그렇게 되면 곧 불안한 기반 위에 더 많은 코드를 쌓게 됩니다. 테스트되지 않은 코드는 더 복잡한 코드를 계속 끌어들이기 마련이고, 결국에는 필요한 테스트 커버리지가 없어서 리팩토링조차 감히 못 하는 상황에 이르게 됩니다. 앞으로의 작업을 위한 안정적인 기반을 만들고 싶었는데, 결과적으로는 유지보수가 불가능한 엉망진창만 남는 거죠.

하지만 이런 상황을 피하는 일은 생각보다 훨씬 쉽습니다! 몇 가지 도구와 약간의 사전 준비만 있으면, 테스트를 코드가 의존하는 서비스로부터 분리하고 더 단순한 코드를 작성하며, 버그를 유발하지 않고 기존 코드를 개선할 자신감까지 얻을 수 있습니다. 다음 테스트를 어떻게 작성해야 할지 몰라 미루는 대신, 코드와 외부 세계 사이의 상호작용을 바라보며 정확히 어디서부터 손대야 할지 알 수 있게 되는 겁니다.

Mocha: 빠르고 간편한 방법

Mocha는 코드와 외부 세계 사이에 끼어드는 가장 쉬운 방법입니다.

예를 들어, Cart 객체가 결제 시점에 신용카드 결제를 처리한다고 해봅시다. 결제가 실패했을 때 카트에 에러 메시지가 붙어 있는지 확인하고 싶습니다.

테스트가 실행될 때마다 실제 결제 시스템을 호출하고 싶지는 않을 겁니다. 설령 그렇게 한다 해도, 해당 서비스가 실패를 반환하도록 강제하는 것 자체가 어려울 수 있죠. Mocha를 사용하면 다음과 같이 작성할 수 있습니다.

def test_error_message_set_on_charge_failure
  cart = Cart.new(items)
  cart.stubs(:charge!).returns(false) # mocha in action
  cart.checkout!
  assert_equal "The credit card could not be charged", cart.credit_card_error
end

Mocha는 메서드가 예상과 다르게 호출되면 테스트를 실패하도록 만들 수도 있습니다.

def test_only_bill_once_per_cart
  cart = Cart.new(items)
  cart.expects(:charge!).once # checkout!을 여러 번 호출해도 이중 결제는 없어야 합니다
  cart.checkout!
  cart.checkout!
end

Mocha는 사용법이 간단하지만 놀랄 만큼 유용합니다. 다만 주의할 점이 있습니다. 원치 않는 동작 모킹해야 한다는 것입니다. 너무 많이 모킹하면 실제 버그를 숨길 수 있습니다. 또한 이 방식에 과하게 의존해서도 안 됩니다. expectsstubs로 가득 찬 테스트는 읽고 이해하기 어렵습니다.

테스트 페이크(Test Fake): 제가 선호하는 방법

같은 객체의 같은 메서드를 반복해서 모킹하거나 스텁 처리하고 있다면, 그 모킹을 완전한 객체 형태로 승격시킬 수 있습니다. 흔히 테스트 페이크(test fake)라고 부르는 방식입니다.

def test_billed_full_amount_minus_discount
  test_payment_provider = TestPaymentProvider.new # 가짜 결제 제공업체
  cart = Cart.new(items, discount: 30, provider: test_payment_provider)
  cart.checkout!

  assert_equal items.sum(:&price) * 0.7, test_payment_provider.total_charges
end

페이크가 훌륭한 이유는 다음과 같습니다.

  • 페이크가 자체 상태를 추적할 수 있습니다

    위 예제의 total_charges 메서드처럼, 페이크는 커스텀 assertion 메시지와 헬퍼 함수를 갖출 수 있어 테스트 작성이 훨씬 쉬워집니다.

  • 완전한 객체이므로 에디터와 언어 차원의 지원을 받을 수 있습니다

    지원하는 에디터를 사용 중이라면, Mocha로 개별 메서드를 스텁 처리할 때는 얻을 수 없는 자동완성, 인라인 문서 등의 기능을 활용할 수 있습니다. 더 나은 유효성 검사, 예외 처리 등 원하는 무엇이든 페이크에 담을 수도 있습니다.

  • 개발 환경에서 페이크를 사용하면 실제 서비스 연결이 필요 없습니다

    버스 안에서도 앱을 개발할 수 있고, 여러 서비스를 띄워 노트북 배터리를 태우지 않아도 됩니다. 또한 페이크 서비스를 설정해 복잡한 셋업 없이도 엣지 케이스를 다루는 데 필요한 데이터를 반환하게 할 수 있습니다.

  • 테스트 밖에서도 활용할 수 있습니다

    이것이 제가 페이크를 가장 좋아하는 부분입니다. 로깅 클라이언트가 서드파티 서비스와 메모리 배열 기반의 페이크 양쪽에 동시에 로그를 남기게 할 수 있습니다. 그리고 이 배열의 내용을 사이트의 관리자 화면에 출력하면, 생각한 대로 로깅되고 있는지 훨씬 쉽게 검증할 수 있습니다.

실제로는 이런 식으로 구현할 수 있습니다.

  fake_backend = FakeBackend.new
  LoggingService.backends = [RealBackend.new, fake_backend]
  LoggingService.debug("TEST MESSAGE PLEASE IGNORE")
  fake_backend.messages.first # => [:debug, "TEST MESSAGE PLEASE IGNORE"]

페이크를 작성하는 것은 개별 메서드를 스텁 처리하는 것보다 더 많은 노력이 듭니다. 하지만 연습하면 유용한 페이크 하나를 만드는 데 한두 시간이면 충분합니다. 다른 사람들에게도 도움이 될 만한 페이크를 만들었다면 공유해 보세요. 저도 오래전에 resque-unit을 만들었는데, 지금도 많은 사람들이 사용하고 있습니다.

그럼 이 객체들은 어떻게 주입할까요?

테스트 대상 객체가 이 페이크들과 통신하도록 만들어야 합니다. 다행히 Ruby는 조작하기 매우 쉬운 언어라서, 페이크를 주입하는 일은 보통 어렵지 않습니다.

테스트 대상 객체의 API를 직접 관리하고 있다면, 기본 매개변수, attribute, 생성자 옵션 중 하나를 추가해 페이크를 설정할 수 있는 지점을 만드는 것이 가장 좋습니다.

class Card
  attr_reader :provider
  def initialize(items, options={})
    @provider = options.fetch(:provider) { RealProvider.new }
  end
end

이 방식은 실제 서비스와 통신할 때도 깔끔하며, 나중에 유연성을 추가할 수 있는 훅(hook)이 됩니다.

객체를 직접 제어할 수 없거나 추가 매개변수를 넣고 싶지 않다면, 언제든지 monkey patch를 사용할 수 있습니다.

# if in test mode
Card.class_eval do
  def provider
    @provider ||= TestProvider.new
  end
end

테스트 환경에서는 다소 지저분해 보이지만, 페이크를 사용하지 않는 환경에서는 더 깔끔하게 유지됩니다.

지금 바로 나만의 페이크를 만들어 보세요

페이크를 만드는 일은 연습할수록 쉬워집니다. 지금 당장 시도해 보세요.

  • 외부 서비스와 통신하는 테스트를 찾으세요. 인터넷 연결이 끊기면 실패하는 테스트가 좋은 후보입니다.
  • 실제 통신을 수행하는 객체가 무엇인지, 그리고 코드가 그 객체에 어떤 호출을 하는지 파악하세요.
  • 해당 클래스의 거의 빈 복제본을 만들고, 호출 내용을 배열에 기록하도록 구현하세요.
  • 페이크에 호출 목록을 반환하는 메서드를 추가하세요.
  • 실제 객체를 새로 만든 페이크 객체로 교체하고, 코드가 수행하는 호출에 대한 assertion을 작성하세요.

직접 시도해 보신다면 결과가 어땠는지 알려주세요!

이러한 기술들을 익히면, 애플리케이션과 외부 세계 사이의 가장 까다로운 상호작용도 금방 길들일 수 있게 됩니다. 적절한 위치에 놓인 단순한 스텁 하나만으로도, 잘 테스트된 코드를 자신 있게 배포할 수 있습니다.