https://d226lax1qjow5r.cloudfront.net/blog/blogposts/testing-external-apis-in-ruby-with-webmock-and-vcr/webmock_vcr.png

RubyでWebmockとVCRを使って外部APIをテストする

最終更新日 November 18, 2021

所要時間:1 分

自動テストは、長い間、ソフトウェア開発とデプロイメントプロセスの不可欠な一部であり、その利点は十分に確立されている。とりわけ、コードの品質を保証するため、リグレッションを防ぐため、そしてテスト駆動開発(TDD)の文脈では、コードを書くプロセスの一部としてテストを使用します。

開発者として、私たちは開発ワークフローの一部として自動テスト・スイートを書くという概念に慣れ親しんでおり、快適です。しかし、アプリケーションが外部APIを利用する場合、テストを書くことには特別な課題が伴います。例を挙げて、そのいくつかを探ってみよう。

例Vonage Messages API で SMS を送信する場合

SMSでテキストメッセージを送信する機能を含むアプリケーションを開発しているシナリオを想像してみてください。これは Vonage Messages API.Messages API を使ってこの機能を実装するには、アプリケーションに MessagesClientメソッドを定義する send_smsメソッドを定義するクラスを含めることができます。このメソッドの目的は、適切にフォーマットされた POSTリクエストを送ることである。 Messages APIエンドポイント:

https://api.nexmo.com/v1/messages

テストに必要な依存関係という意味では、私たちの Gemfileテストに必要な依存関係という点では、次のようになるでしょう(実際には、アプリケーションにはさらにいくつかの依存関係が含まれるでしょう)。

Gemfile

# Gemfile

source "https://rubygems.org"
ruby "3.0.0"

gem 'faraday'

group :test do
  gem 'rspec'
end

ファラデー ファラデーgemはHTTPリクエストとレスポンスを管理するためのRubyライブラリです。

アプリケーション・ファイルは MessagesClientクラスと send_smsメソッドで定義します。

app.rb

# app.rb
require "json"

class MessagesClient
  URI = 'https://api.nexmo.com/v1/messages'

  def send_sms(from_number, to_number, message)
    headers = generate_headers(message)
    body = {
      message_type: 'text',
      channel: 'sms',
      from: from_number,
      to: to_number,
      text: message
    }
    Faraday.new.post(URI, body.to_json, headers)
  end

  # additional methods omitted for brevity

end

上記の例では send_smsメソッドは、メッセージ・テキスト、to 番号、from 番号のパラメータを定義しています。これらの詳細をメッセージ bodyで送信される。 POSTリクエストをAPIエンドポイントに送信します。 postメソッドを使ってAPIエンドポイントに送信されます。そして send_smsメソッドはFaradayインスタンスが受け取ったHTTPレスポンスを表すオブジェクトを返します。

では、このメソッドをテストするにはどうすればよいだろうか?ハッピーパスをテストするための一つのアプローチは、メソッドを呼び出すテストを書いて、ライブAPIエンドポイントをヒットし、成功を示すレスポンスコードを受け取ることをアサートすることだろう。Messages API v1の場合、これはHTTPレスポンスコードである 202.

messages_client_spec.rb

require "spec_helper"

describe MessagesClient do
  let(:app) { MessagesClient.new }

  describe "#send_sms" do
    let(:from_number) { "447700900000" }
    let(:to_number) { "447700900001" }
    let(:message) { "Hello world!" }

    it "returns status 202 Accepted" do
      response = app.send_sms(from_number, to_number, message)

      expect(response.status).to eq 202
    end
  end
end

このテスト方法にはいくつかの問題がある。

まず第一に、データベースや外部APIなどの外部依存とやりとりするテストは、そうでないテストよりもずっと遅くなる可能性が高い。このテストの実行例を見てみましょう。 1.63秒かかる。

Finished in 1.63 seconds (files took 0.36557 seconds to load)
1 example, 0 failures

このようなことは それほど遅く感じないかもしれないがこれは一つのメソッドに対する一つのテストである。アプリケーションの大きさや複雑さによっては、同じようなテストがいくつもあるかもしれません。そのようなシナリオでは、テスト・スイート全体を実行するのは苦痛を伴うほど遅くなるでしょう。

第二に、私たちのテストはHTTPリクエストがネットワーク経由で送信され、外部の依存関係によって処理されることに依存しているため、有効なレスポンスが受信されること、あるいは実際にレスポンスがまったく受信されないことを確信することはできません。ネットワークの問題や、一時的な停止、あるいは私たちがコントロールできない他の外部要因があるかもしれません。 毎回テストを実行するたびに、期待された応答を受け取れない可能性があります。

自動テストを書くためのベストプラクティスや、さまざまなタイプのテストが何をすべきで何をすべきでないかについては、多くの議論があります。しかし、一般的に合意されている原則は、特にユニットテストや小規模な統合テストに取り組む場合、テストを 速くそして 決定性.

テストのピラミッド」の下に行くほど、テストの数は増え、実行する頻度も増える。したがって、このレベルでは高速テストが重要である。さらに、テストを開発プロセスの一部として、あるいは初期のフィードバックのために使用する場合、テストが決定論的であることが重要です。言い換えれば、特定の入力が提供されたときに、テストがあらかじめ決められた出力を生成することを確認したいのです。

これらの原則を私たちのテストの文脈で考えると、次のような問題がある。 send_smsテストの文脈で考えると、問題が生じる。私たちはテストが高速で決定論的であることを望んでいるが、ライブのAPIエンドポイントを叩くことに伴う問題は、これらの原則に反する。

外部APIを使用する際に考慮すべき点は他にもあります。 POSTこのメソッドのリクエストは、テストを実行するたびに実際のSMSメッセージを作成します)、あるいはコストやレート制限の潜在的な問題などです。これらの追加的な問題の多くは、APIサンドボックスを使うことで対処できる。 はサンドボックスを提供している。

サンドボックスを使うのは、エンド・ツー・エンドテストや機能テスト、大規模な統合テストなど、ピラミッドの上位に位置するテストに適している。低レベルのテストに本当に必要なのは、次のような方法です。 外部依存にまったく触れない.その解決策のひとつが モッキング.

モッキング

この用語に馴染みのない人のために説明すると、モックとはテストにおけるテクニックのひとつで、 アプリケーションの内部あるいは外部からの実際のレスポンスの代用として、モックあるいは「偽の」 レスポンスを使うことです。内部レベルでは、これはメソッドや関数の呼び出しによって返されるモックオブジェクトを意味します。外部 API のコンテキストでは、一般に実際の HTTP レスポンスをモックしたもので置き換えることを意味します。

外部APIを使う場合のこのアプローチの大きな利点は、リクエストをネットワーク経由で送信してレスポンスを待つ必要がないため、テストの実行が格段に速くなることだ。さらに、HTTPレスポンスをあらかじめ定義しておくことで、テストを決定論的なものにすることができる。

モッキングは、現在のテスト・セットアップで明らかになった問題点を解決するのに理想的に思えます。 send_smsテストにどのように実装すればいいのでしょうか?

ウェブモックの紹介

ウェブモックはHTTPリクエストのスタブや期待値を設定するためのRubyライブラリです。様々なHTTPライブラリをサポートしており、以下のような様々なテストフレームワークに統合することができます。 rspec.

ハイレベルなメンタル・モデルとして、Webmockは基本的に以下のことを行う:

  • アプリケーションから発信される HTTP リクエストをすべて傍受する。

  • これらのリクエストを、事前に登録された「スタブ」と照合する。

  • 実際の HTTP レスポンスの代わりに、そのリクエストに対して事前に定義された レスポンスを返します。

テスト・セットアップにWebmockを追加して、このメンタル・モデルを実際に試してみよう。

更新された例WebmockでHTTPレスポンスをモックする

まず webmockを追加して Gemfileを追加して bundle install.

Gemfile

# Gemfile

source "https://rubygems.org"
ruby "3.0.0"

gem 'faraday'

group :test do
  gem 'rspec'
  gem 'webmock'
end

注:また require 'webmock/rspec'を追加する必要があります。 spec_helper.

そうすれば、Webmockを使うようにテストを更新できる。

messages_client_spec.rb

require "spec_helper"

describe MessagesClient do
  let(:app) { MessagesClient.new }

  describe "#send_sms" do
    let(:from_number) { "447700900000" }
    let(:to_number) { "447700900001" }
    let(:message) { "Hello world!" }

    it "returns status 202 Accepted" do
      stub_request(:post, "https://api.nexmo.com/v1/messages").to_return(status: 202)
      response = app.send_sms(from_number, to_number, message)

      expect(response.status).to eq 202
    end
  end
end

更新されたテストでは スタブを登録します。 stub_requestメソッドを使います。このスタブは POSTリクエストにマッチし https://api.nexmo.com/v1/messagesにマッチし :statusを返します。 202.

Webmockは発信するHTTPリクエストをインターセプトし、代わりにあらかじめ決められたレスポンスを返します。 以前より以前よりずっと速くなりました:

Finished in 0.00749 seconds (files took 0.50786 seconds to load)
1 example, 0 failures

モッキングの限界

Webmockを追加することで、我々のテストは高速かつ決定論的になった。しかし、外部APIのテストにモッキング・ツールを使うことにはいくつかの注意点があります。

モックは書くのに時間がかかることがある

この例のモックは :statusコードを定義しているだけです。他のモックではレスポンスも定義する必要があるかもしれません。 :headers:body.テストするAPIによっては、これらのヘッダやボディはかなり大きく複雑になり、モックの中で定義するのにかなりの時間を必要とします。

これを複数のAPIエンドポイントに対する複数のテストに拡大すると、モックを書くのにかなりの時間を費やすことになる。

モックはメンテナンスが難しい

この最初のポイントに関連するのが メンテナンス.外部APIは時間とともに変更され、新しい機能が追加されたり、新しいバージョンがリリースされたりすることがある。例えば、Vonage Messages API は最近新しいバージョンをリリースした。.テスト用の複雑なモックを大量に持っている場合、それらのモックをメンテナンスして変更に対応しようとすると、多くの時間と労力が必要になります。

モックは依存関係について誤った仮定をするかもしれない。

モックは モックはを表現するために書かれます。 実際のモックは実際のレスポンスではなく、特定のレスポンスを表すために書かれるものなので、 実際のレスポンスがどのようなものであるかという仮定に基づいています。しかし、モックするレスポンスが複雑になってくると、そのレスポンスについて間違った仮定をする可能性も出てきます。そのような間違った仮定は、モックされたテストには合格するものの、正しく動作しないコードにつながる可能性があります。うまくいけば、そのような問題を発見できるようなピラミッドの上位のテストがあるのですが、理想を言えば、下位のテストでできるだけ早く発見したいものです。

これらの制限に対処するために、別のRubyライブラリに目を向けることができる:VCRである。

ビデオレコーダー

VCRは、HTTP リクエストとレスポンスの「記録と再生」というテストパターンに従ったライブラリです。基本的には、テストスイートの HTTP インタラクションを記録し、将来のテスト実行時にそれを再生します。

VCRは、「カセット」(旧式のビデオレコーダーのカセットテープに基づく)のアイデアを使用して、このパターンを実装しています。 ビデオカセットレコーダー技術に基づく)。それぞれの 'カセット' はファイルであり、特定の HTTP インタラクションの記録を表すデータを含んでいます。テストが最初に実行されると、実際の HTTP リクエスト/レスポンスのサイクルが発生し、このサイクルの詳細がカセットとして記録されます。

カセットはリクエストと応答の両方に関する情報を含む。リクエストデータはその後のテストの実行時にリクエストのマッチングに使用され、 応答データはそれらの実行時に期待される応答のモックに使用される。モック'は 実際のモック'は実際のレスポンスデータを使うので、モックを書いて保守する時間や、モック を作る際の前提条件について、先に説明した問題に対処することができます。

このようなVCRの働きは、私たちのテストセットアップの文脈で検証すれば、視覚化しやすいかもしれない。

更新された例テストセットアップにVCRを組み込む

まず vcrを追加して Gemfileに追加して bundle install

Gemfile

# Gemfile

source "https://rubygems.org"
ruby "3.0.0"

gem 'faraday'

group :test do
  gem 'rspec'
  gem 'webmock'
  gem 'vcr'
end

そして、VCRを使用するようにテストを更新することができる。

messages_client_spec.rb

require "spec_helper"
require "vcr"

VCR.configure do |config|
  config.cassette_library_dir = "spec/cassettes"
  config.hook_into :webmock
  config.configure_rspec_metadata!
end

describe MessagesClient do
  let(:app) { MessagesClient.new }

  describe "#send_sms" do
    let(:from_number) { "447700900000" }
    let(:to_number) { "447700900001" }
    let(:message) { "Hello world!" }

    it "returns status 202 Accepted" do
      response = VCR.use_cassette('send_sms') do
        app.send_sms(from_number, to_number, message)
      end

      expect(response.status).to eq 202
    end
  end
end

このファイルでは vcrを要求し、VCRのセットアップを設定する。 spec_helperファイルに移すことができる)。

  • コンフィギュレーションは cassette_library_dir設定はVCRに「カセット」の保存場所を指示する。ここではディレクトリ spec/cassettesこのディレクトリが存在しない場合は、VCRが作成します。

  • コンフィギュレーションは hook_intoコンフィギュレーションは、HTTPリクエストにどのようにフックするかをVCRに指示する。すでに webmockを依存関係として持っているので、ここでそれを指定する。 Gemfileから削除して、代わりにFaradayに直接フックすることもできる)。

その他にも多くの設定オプションがある。 その他の設定オプションがあります。

このテストでは、Webmockのスタブリクエストを削除しています。代わりに response変数にVCRの use_cassetteメソッドの返り値を変数に設定します。このメソッドには引数としてカセット名とブロックを渡します。デフォルトの録画設定では、もしカセットが存在すれば、VCR はそれを使ってレスポンスオブジェクトを作成し、それをテストでアサートします。カセットが存在しない場合、VCR はブロックを呼び出し、それが返す値を使用してカセットを作成します。

最初の頃 最初にテストを最初に実行するとき、その時点ではカセットは存在しないので、APIエンドポイントをヒットし、VCRはそのHTTPインタラクションを使用してファイルを作成する。 send_sms.ymlファイルを作成し、HTTPリクエストとレスポンスの詳細を保存します。

send_sms.yml

---
http_interactions:
- request:
    method: post
    uri: https://api.nexmo.com/v1/messages
    body:
      encoding: UTF-8
      string: '{"message_type":"text","channel":"sms","from":"447700900000","to":"447700900001","text":"Hello
        world!"}'
    headers:
      User-Agent:
      - Faraday v1.8.0
      Authorization:
      - Basic xxxxxxxxxxxxxxxxxxxxxxxxxxx==
      Content-Type:
      - application/json
      Host:
      - api.nexmo.com
      Content-Length:
      - '12'
      Accept-Encoding:
      - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
      Accept:
      - "*/*"
  response:
    status:
      code: 202
      message: Accepted
    headers:
      Server:
      - nginx
      Date:
      - Thu, 18 Nov 2021 12:35:49 GMT
      Content-Type:
      - application/json
      Content-Length:
      - '55'
    body:
      encoding: UTF-8
      string: '{"message_uuid":"c26213be-2916-4c64-903e-4125158eedd8"}'
  recorded_at: Thu, 18 Nov 2021 12:35:49 GMT
recorded_with: VCR 6.0.0

テストが最初に実行されるとき、APIエンドポイントを叩いているため、WebmockやVCRをまったく使っていないときと同じように実行が遅くなる。

Finished in 1.61 seconds (files took 0.58899 seconds to load)
1 example, 0 failures

それ以降の 以降のすべてのVCR はカセットを使用します。まず、テスト実行のリクエスト詳細がカセットに記録されたものと一致するかどうかを確認します。もし 一致した場合一致した場合、カセットの応答詳細が応答オブジェクトの作成に使用されます。この場合、記録されている応答コードは 202であるため、これを statusとして設定されます。このテストでは response.statusと等しくなければなりません。 202と等しいはずだと主張しているので、テストはパスします。

この後のテスト走行もまた はるかにより速い。

Finished in 0.01033 seconds (files took 0.55884 seconds to load)
1 example, 0 failures

VTRのヒントとコツ

VCRは設定オプションの面で多くの柔軟性を提供する。

リクエストマッチングの設定

録画を再生するために、VCRは新しいHTTPリクエストと以前に録画されたものの詳細を照合する必要がある。その マッチングはリクエストの様々な要素に対して行うことができる。

デフォルトのコンフィギュレーションはHTTPメソッドとURIに対してマッチするものだが、このコンフィギュレーションを修正して、(完全なURIではなく)ホストとパスを別々にマッチさせたり、クエリパラメータ、リクエストヘッダ、リクエストボディをマッチさせたりすることもできる。

さらに、カスタム・マッチャーを作成して、さらに柔軟性を高めることもできる。

フェード・アウェイではなく、再録音

前述したように、外部APIは時間とともに変更されたり、新しいバージョンがリリースされたりすることがある。そうなった場合、モック全体を書き直すのではなく(標準的なモッキングのセットアップで行うように)、古くなったものを置き換えるために新しいHTTPインタラクションを記録することができます。再録音のアプローチにはいくつかの方法があります:

  • 最も強引な方法は、現在の録画のファイルを削除することである。特定のテストに対する録画が存在しない場合、VCRは自動的に新しいものを録画する。

  • また、様々な 録画モードがあります。例えば :once(デフォルト) はカセット・ファイルがない場合にのみ新しいインタラクションを記録します。 :new_episodesは、テスト用の既存のファイルがあるにもかかわらず、そのテストのリクエスト詳細がファイルに記録されているものと完全に一致しない場合に、新しいインタラクションを記録します。

  • 自動再記録を有効にすることで、定期的にやりとりを再記録することができます。特定のカセットのセットアップで :re_record_intervalオプションを設定できます。そのカセットが使用されると、VCRはカセット内のタイムスタンプを現在の時刻と照合します。 recorded_atのタイムスタンプを現在の時刻と照合します。で指定された時間以上経過している場合、そのやりとりは再録されます。 :re_record_intervalで指定された時間以上経過していれば、そのインタラクションは再録されます。

機密データに注意

外部APIとやりとりする際、そのAPIの認証方法によっては、APIキーのような機密データをリクエストの一部として含む可能性があります。 認証ヘッダなどである。このデータは、対話の一部としてVCR録画に存在することになる。プロジェクトのコードをGitHubの公開リポジトリにプッシュするなどして一般に公開している場合、これは問題になる。

ひとつの解決策は、個々のレコーディング、あるいはディレクトリ全体を cassettesディレクトリを .gitignoreファイルに追加することだ。あるいは、VCRの filter_sensitive_data 設定オプションを使って、あるデータの置換文字列を指定し、それを実際のデータの代わりに録画に表示させることもできる。

ドキュメントを利用する

VCRは 詳細な使用法を提供しています。 APIドキュメントを提供しています。

代替ツール

ここで取り上げるツールはRubyのエコシステムの中で確立されたものですが、Rubyistにも非Rubyistにも利用可能な代替ツールもたくさんあります。

モッキング機能に関しては rspec-mocks ライブラリはモッキング機能を rspec.HTTPライブラリーの中には ファラデーのような HTTP ライブラリは、スタブリクエストを定義できるアダプタを提供しています。Faraday のモック機能は VCR とも互換性があります。

さらに、以下のような他のプログラミング言語へのVCRの移植も数多くある。 vcrpyPython用の php-vcrPHP用の scotchそして Betamax.Netがある。 NockはNode.NetのVCRとWebmockの組み合わせに似た機能を提供します。

他の言語への移植は、VCRの READMEに記載されているので、どの言語を使うにしても、うまくいけば録画再生ツールがあるはずだ。

ハッピーテスト!

シェア:

https://a.storyblok.com/f/270183/373x376/e8d3211236/karl-lingiah.png
Karl LingiahRuby開発者支援

KarlはVonageのDeveloper Advocateで、RubyサーバSDKのメンテナンスとコミュニティの開発者エクスペリエンスの向上に注力しています。彼は学ぶこと、ものを作ること、知識を共有すること、そして一般的にウェブ技術に関連することが大好きです。