Railsアプリでの Stripe API と Webhook の自動テスト

## はじめに

決済サービス Stripe は開発環境がよくできています。

- 本番環境とほぼ同じように使える[テスト環境](https://docs.stripe.com/test-mode)が用意されている
- 管理画面でイベント/API 呼び出し/Webhook のログが確認できる
- [Stripe CLI](https://docs.stripe.com/stripe-cli) を使うと開発 PC に Webhook を転送することができる
- [テストクロック](https://docs.stripe.com/billing/testing/test-clocks)を使うと時間の進行のシミュレーションが可能(例えば、サブスクリプションの支払いのテストができる)

など、非常に便利な機能が用意されています。

しかし、Stripe API を使った機能の自動テストを書く際には、 Stripe が用意した機能だけですと面倒な部分もあります。この記事では、メンテナンスしやすい Stripe 決済の自動テストを書く方法について説明します。

特に Webhook のテストのために自作した gem [vcr_stripe_webhook](https://github.com/shunichi/vcr_stripe_webhook) について説明します。

## Stripe の API をモックする

決済機能の自動テストを書こうとすると、Stripe API の呼び出し部分をどうテストするかが問題になります。
本当に API を呼び出してしまうと、そのサービスの状態に依存してしまいますし、テストの API 呼び出しで作成されたオブジェクトがテスト環境に際限なく溜まっていきます。
また、API 呼び出しのレート制限もあるため、あまり頻繁に実際の API を使用することは推奨されていません。

[自動化されたテスト | Stripe のドキュメント](https://docs.stripe.com/automated-testing) には以下のように書かれています。

> Stripe API の応答を確認するためのテストが必要になったときに、テスト環境で API にリクエストを行うアプローチも許容されます。また、Stripe API リクエストを使用して Stripe API の応答が変更されていないことを定期的に検証することもできます。ただし、**レート制限を避けるためこれらのテストは頻繁に行うべきではありません**。

実際に API を呼び出すことなく API をテストするにはモックを使うのが一般的です。モックを使うのには、いくつかの選択肢があります。

- API の応答を手動で保存して使用する
- 非公式の [stripe-ruby-mock gem](https://github.com/stripe-ruby-mock/stripe-ruby-mock) を使用する
- 公式の[stripe mock](stripe-mock)を使用する
- [vcr gem](https://github.com/vcr/vcr) を使ってテスト環境の API 呼び出しを記録する

「API の応答を手動で保存して使用する」方法はメンテナンスしづらいため避けたいです。

stripe-ruby-mock は対応している API が限られますし、Stripe 側の状態を完全にエミュレートしているわけではなく、複数の API をまたいだときに期待するレスポンスが得られないことがありました。fork して期待するレスポンスを返すように改造することもできますが、長期的なメンテナンスを考えるとつらいです。アクティブにメンテナンスされておらず、以前 Pull Request を送ったことがありますが、返答はなく、マージしてもらえませんでした。

stripe mock はハードコードされたレスポンスを返すだけで、リクエストに対応したデータは返さないようです。自動テストには使えません。

vcr gem を使えば、実際の Stripe テスト環境に対する API 呼び出しを記録し、モックデータとして保存できます。保存後は実際の API を呼び出すことなく、記録されたレスポンスを返すので、レート制限や Stripe 側の状態に依存せずテストを実行可能です。

結論としては vcr gem を使うのがおすすめです。この記事では vcr gem の使い方については解説しません。[公式のドキュメント](https://benoittgt.github.io/vcr)もありますし、ググれば解説記事が見つかると思いますので調べてみてください。

## Webhook のテスト

Stripe の決済において Webhook は欠かせないものです。 例えば Stripe Checkout による決済の成功は必ず Webhook で受け取る必要があります。また、サブスクリプションの毎月の支払い成功/失敗は、通常は Stripe からの Webhook で受信します。

vcr を使えば Stripe API の応答を記録することができますが、Webhook は vcr では記録できません。vcr はアプリケーションから外部サービスの API 呼び出しを記録することはできますが、外部サービスからアプリケーションへの HTTP リクエストを記録はできません。

Webhook だけはあきらめて手動でモックデータをメンテナンスするというのも手ですが、 vcr のように実データを記録できると、メンテナンス性の意味でも、テストの信頼性の意味でも嬉しいです。

そこで [vcr_stripe_webhook](https://github.com/shunichi/vcr_stripe_webhook) という gem を作りました。

### vcr_stripe_webhook の使い方

まずは必要なソフトウェアをインストールしてください。

- [vcr](https://github.com/vcr/vcr) gem (Gemfile に記述)
- [webmock](https://github.com/bblimke/webmock) gem (Gemfile に記述)
- [vcr_stripe_webhook](https://github.com/shunichi/vcr_stripe_webhook) gem (Gemfile に記述)
- [Stripe CLI](https://docs.stripe.com/stripe-cli) (vcr_stripe_webhook が webhook を受信するために必要です)

Stripe テスト環境の API キーを stripe gem に対して設定します。

```ruby
# config/initializers/stripe.rb などで Stripe のAPIキーを設定します。
# dotenv gem などを使って環境変数からAPIキーを読むのがよいです。
# テストでは Stripe のテスト環境用のAPIキーを使うようにしましょう。
Stripe.api_key = ENV.fetch('STRIPE_API_KEY')
```

rspec などのテストで `vcr.use_cassette` の代わりに `VcrStripeWebhook.use_cassette` 使って、以下のように書きます。

```ruby
# VCR.use_cassette の代わりに VcrStripeWebhook.use_cassette を使います
# このメソッドは内部で VCR.use_cassette を呼んでいます。
VcrStripeWebhook.use_cassette("create_customer_and_attach_payment_method") do |vcr_cassette|
  stripe_customer = nil

  # 受信したい Webhook イベントの名前を指定して、 VcrStripeWebhook.receive_webhook_events を呼びます。
  # ブロックの中で Webhook を発生させる API を呼びます。
  # 受信した Webhook の内容は戻り値として得られます。
  webhook_events = VcrStripeWebhook.receive_webhook_events(
    event_types: %w[customer.created payment_method.attached]) do

    customer_params = {
      email: "test-user@example.com",
      name: "test-user"
    }
    stripe_customer = Stripe::Customer.create(customer_params)

    stripe_payment_method = Stripe::PaymentMethod.retrieve("pm_card_visa")
    stripe_payment_method.attach(customer: stripe_customer.id)
  end

  # Webhook を自分のアプリケーションのエンドポイントに対して送ります。
  webhook_events.each do |webhook_event|
    post your_stripe_webhook_path, params: webhook_event.as_json,
      headers: { 'Content-Type: application/json' }
  end

  # Webhook の結果、DBに記録された情報をここでテストします
  ...
ensure
  # 必須ではないですが、テストで作ったオブジェクトの削除をしておくと、
  # Stripeテスト環境に不要なデータがたまらないので良いです。
  stripe_customer&.delete
end
```

カセット(HTTP リクエスト/レスポンスを記録した YAML)がない状態でテストを実行すると、実際に Stripe の API が呼ばれ、vcr によって HTTP リクエスト/レスポンスが記録され、 vcr_stripe_webhook によって Webhook が記録されます。
vcr_sripe_webhook のカセットは `{vcrカセット保存ディレクトリ}/stripe_webhooks/` 以下に保存されます。

カセットがすでにある場合は、実際の API は呼ばれずカセットに記録された情報が使われます。

stripe gem のバージョンアップや、Stripe の API バージョン変更の際は、リクエストやレスポンスが変わる可能性があるため、一度カセットの YAML を削除して実 API の記録をやりなおすとよいです。

## まとめ

この記事では、Rails アプリでメンテナンスしやすい Stripe API や Webhook の自動テストを書く方法について説明しました。

- vcr gem を使うと API のリクエスト/レスポンスを記録し、サーバーの状態に依存せず、API レート制限を気にせずテストが実行できます
- vcr_stripe_webhook gem を使うと Webhook も記録できるので、よりメンテナンスしやすいテストが書けます

## おまけ (vcr gem の設定サンプル)

```ruby
VCR.configure do |c|
  c.cassette_library_dir = 'spec/vcr'
  c.hook_into :webmock

  # VCRカセットを使用していない場合はHTTPリクエストを素通しする
  c.allow_http_connections_when_no_cassette = true
  # VCRで記録しないHTTPリクエスト先ホスト名
  c.ignore_hosts('require-real-request.example.com')
  # ローカルホストへのHTTPリクエストはVCRで記録しない
  c.ignore_localhost = true

  # VCRのカセットに実際の Stripe API キーが保存されないようにする
  c.filter_sensitive_data('<STRIPE_API_KEY>') { Stripe.api_key }

  # VCRを使うテストに `it 'example name', :vcr do` のようにvcrタグをつけておき、
  # 以下のように rspec を実行すると、VCRを使うテストだけを再実行し、実APIの記録をしなおすことができます。
  # (VCR_RECORD=all をつけると、カセットYAMLを削除しなくても実APIを記録しなおします)
  #
  #   VCR_RECORD=all bundle exec rspec --tag vcr
  #
  # SEE:
  #   https://benoittgt.github.io/vcr/#/record_modes/all
  #   https://benoittgt.github.io/vcr/#/record_modes/once
  c.default_cassette_options[:record] = ENV.fetch('VCR_RECORD', 'once').to_sym
  # これを true にしないと古い記録と新しい記録がマージされておかしなことになる
  c.default_cassette_options[:drop_unused_requests] = true
end
```