RSpecでHTTP通信を記録するVCRの簡単な設定を考える

## はじめに

[VCR]という gem を使うと、[RSpec]のテスト実行中に API を直接叩かず、代わりに過去のリクエスト・レスポンスの記録(カセットと呼ばれるYAMLファイル)を繰り返し使用することができます。

[VCR]には色んな設定項目がありますが、個人的には結局はシンプルな使い方しかしないことが多いので、[VCR]の基本機能を簡単に使える設定を考えてみました。

[VCR]の細かい機能については[公式ドキュメント][VCR]やネット上のブログ記事をご参照ください。

https://benoittgt.github.io/vcr

https://www.rubydoc.info/github/vcr/vcr

[RSpec]: https://rspec.info/
[RSpec - Hooks]: https://rspec.info/documentation/3.13/rspec-core/RSpec/Core/Hooks.html
[RSpec - Metadata]: https://rspec.info/documentation/3.13/rspec-core/#metadata
[RSpec - it]: https://rspec.info/documentation/3.13/rspec-core/RSpec/Core/ExampleGroup.html#it-class_method
[VCR]: https://benoittgt.github.io/vcr
[VCR.use_cassette]: https://www.rubydoc.info/github/vcr/vcr/VCR:use_cassette
[VCR - Cassettes]: https://benoittgt.github.io/vcr/#/cassettes/allow_unused_http_interactions?id=background

## 実行環境

- OS: [LMDE 6 (faye) x86_64](https://linuxmint.com/edition.php?id=308)
- ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux]
- rails-7.1.3.2
- webmock-3.23.1
- vcr-6.3.1
- rspec-core-3.13.0
- rspec-rails-6.1.3

## やりたいこと

- [RSpec] の [フック（hooks）][RSpec - Hooks] を利用して VCR 機能の入り切りをする
- [カセット(cassete)][VCR - Cassettes]の名前を spec ファイル名を元に自動で生成
- spec ファイル名を元に自動的に適切なカセットを使用し[VCR.use_cassette]を自動的に実行
- 環境変数、もしくは [RSpec] の [メタデータ（metadata）][RSpec - Metadata] からカセットに記録するかしないかを切替
- [RSpec] の メタデータ から[VCR]の設定をオーバーライドできるようにする

## 論より Run

手を動かして、色々試した結果をリポジトリに残しました。

https://github.com/mnishiguchi/rspec_vcr_example_app

### 設定ファイル

ここでは`spec/support/vcr.rb`を作り、そこに[VCR]の設定を記述しています。

[VCR]自体の設定より、[RSpec]の設定を工夫しました。

```rb:spec/support/vcr.rb
require 'vcr'

# VCRの設定
VCR.configure do |c|
  c.cassette_library_dir = 'spec/cassettes'
  c.hook_into :webmock
  c.ignore_localhost = true
  c.ignore_hosts "chromedriver.storage.googleapis.com"
  c.default_cassette_options = {
    record: :once,
    match_requests_on: %i[method host path query]
  }
end

# RspecにVCR関連オプションを渡す仕組みと自動的にVCRを使いHTTP通信を記録する仕組みを定義
RSpec.configure do |c|
  c.around(:each, :vcr) do |example|
    cassette_path_segments = example.metadata[:file_path].sub(%r{.*/spec/}, '').sub('.rb', '').split(File::SEPARATOR)
    cassette_path = File.join(cassette_path_segments)

    case example.metadata
    in { vcr: true }
      vcr_overrides = {}
    in { vcr: { **vcr_overrides } }
      vcr_overrides[:record] = :new_episodes if vcr_overrides[:record] == true
    end

    vcr_overrides[:record] = :new_episodes if ENV['VCR_RECORD']

    VCR.use_cassette(cassette_path, vcr_overrides) { example.call }
  end
end
```

### 基本的な使い方

`{ vcr: true }` を [RSpec] の [メタデータ][RSpec - Metadata] に渡すことにより、[VCR]の機能を有効化します。

```rb
describe ExampleApiClient, vcr: true do
  describe "#list_todos" do
    it "responds with 200 status" do
      response = described_class.new.list_todos()

      expect(response.status).to eq(200)
      expect(response.body).to be_present
    end
  end
end
```

RSpecの仕様なのか `:vcr` メタデータの渡し方は複数あります。 

細かい設定を指定せずに VCR を有効化したい場合は、以下のどれでもOKです。

- `describe ExampleApiClient, :vcr do ...`
- `describe ExampleApiClient, vcr: true do ...`
- `describe ExampleApiClient, vcr: {} do ...`

### カセットに Web 通信を記録する方法

カセットに Web 通信を記録する方法は２パターン想定しています。

1. 一時的に`{ vcr: { record: true } }`を [RSpec] の メタデータに渡す
1. 一時的に環境変数`VCR_RECORD=1`をつけて`rspec` コマンドを実行する

カセットファイルは spec ファイルのファイル名を元に決定的に生成します。例えば、spec ファイルが `./spec/services/my_api_client_spec.rb`の場合、カセット名は`./spec/cassettes/services/my_api_client_spec.yml`となります。

### VCRオプションのオーバーライド

`:record`オプションと`:match_requests_on`オプションはメタデータに渡しオーバーライドできるようにしてあります。

```rb
describe ExampleApiClient, vcr: { record: :new_episodes } do
  describe "#list_todos" do
    it "responds with 200 status" do
      # ...snip...
    end
  end
end
```

:tada::tada::tada:

## VCR の configure_rspec_metadata!

VCRには[configure_rspec_metadata!]メソッドが存在し、ここで実施したことと似たようなことができるようです。

```ruby
VCR.configure do |c|
  # ...snip...
  c.configure_rspec_metadata!
end
```

[configure_rspec_metadata!]の提供する機能で問題ないのであれば、１行のコードで解決しますが、[configure_rspec_metadata!]は公式ドキュメントに詳しく説明されておらず挙動がよくわかりません。ソースコードを読んでみましたが、それでもよくわかりませんでした。

個人的には10行程度のコードで自作できるのであれば、自分で書いたほうがいいんじゃないかな〜と思ってます。

[configure_rspec_metadata!]: https://www.rubydoc.info/github/vcr/vcr/VCR/Configuration#configure_rspec_metadata!-instance_method

## おわりに

[RSpec]で[VCR]を簡単に使う方法を共有させていただきました。

なにか情報があれば、ぜひお便りください :bow: