今さらVCR使ってみた

本日の素材

vcr/vcr: Record your test suite’s HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.

まとめ

  • WebMock で HTTP を hook して、VCR で WebMock に hook する
  • あとは置き場所を設定しておくだけで自動的に HTTP が record されて replay される
    • WebMock 不要な場合もアリ(利用している HTTP クライアントライブラリ次第)
  • response の差し替えのために ERB を有効にしておく
  • これで HTTP request 自体を直接 stub out できなくてもテストを書ける

背景

GitHub の release からのダウンロードを自動化したかった。

最初スクレイピングするのか?とか思ったけど、さすがになんか API があるじゃろと思い、数年前に挫折した Octokit を試してみることにした。

いろいろガチャガチャやってみて、使う道具としては

辺りかなとなった。

ハッピーパスはだいたい動くところまできて、

zip の展開までやりたいが、さすがに本物の zip ダウンロードして展開するのはテストコードでやりたくないよね

というところで、ハタと困った。Ocktokit や Down のバックエンドの Faraday, Net::HTTP を都合よく stub out できない。しゃーねぇ、名前だけ知ってた Webmock 使うかと入れてみたらすべての HTTP リクエストを capture しようとしているのか、定義がないと怒られてしまう。

そこで改めて Webmock について調べてみると VCR という名前を見かけた。VCR そういや、そんなのあったな。というくらいの知識。HTTP リクエストを記録してリプレイする機能を提供するもので、確かに今回は

一度本物をダウンロードしたあと、ダウンロードするファイルを差し替えてしまえば zip の展開までテストできるな

と思いついたのでやってみることに。

設定自体はいたって簡単

今回は

を利用する。下のような感じ。

require "webmock/minitest"
require "vcr"

VCR.configure do |c|
  c.cassette_library_dir = File.join(__dir__, "/fixtures/cassettes")
  c.hook_into :webmock
  c.default_cassette_options = {erb: true}
end

必要なディレクトリは存在しなければ自動的に作成される。

今回は ERB も利用したかったので上のようになった。

WebMock利用時の注意点

上の Minitest と組み合わせる設定だと WebMock は常時 enable になっている。

require "webmock/minitest"

の中で

require 'webmock'

WebMock.enable!

となっているため。もしこれが都合が悪いなら自分で WebMock を利用するテストとそうでないテストの準備をすること。

パッと分からなかったUnhandledHTTPRequestError

VCR.use_cassette() do
end

の block の位置が問題だった。

it {
  ...
  VCR.use_cassette() do
    ...
  end
}

で VCR block の外にコードを書いて、そこで生成されたオブジェクトを block 内から参照するという書き方をしていたんだけど、ダメっぽい。

Minitest::UnexpectedError:         RuntimeError: Neutered Exception VCR::Errors::UnhandledHTTPRequestError:

Minitest でテストを書いていたんだけど、このエラーだと意味がうまく読み取れなかった。

VCR の block の外にコードを置くこと自体は、VCR が capture しようとする HTTP リクエストが発生していなければ問題ないんだけど、Octokit など 3rd party のライブラリを利用して、いつ HTTP リクエストが発生しているのか自分でもよく分かっていない場合に「cassette で再現できない HTTP リクエストがあるよ」問題になってしまう。

最終的には Octokit の初期化のタイミングを見直し、before block で共通化できるものはし、できない部分だけ VCR.use_cassette block の中に書く形

に落ち着けることができたが、少し戸惑うところだなと思った。

具体的にfile downloadのresponseを差し替える

これは実際のコードを見てもらうと早いと思うんだけど、

  response:
    body:
      encoding: ASCII-8BIT
      string: ''

この部分を書き換えてやる。ERB と組み合わせるとこんな感じで実現できる。

  response:
    body:
      encoding: ASCII-8BIT
      string: !binary: |
        <%= Base64.strict_encode64(File.binread("/path/to/zipfile.zip")) %>

これで実際に download することなく、かつ巨大な ZIP ファイルを cassette の中に埋め込んでしまうことなく、好きな内容のファイルを使ってテストを動かすことができる。ファイルは置き場所と名前を変えなければ内容は好きに変えて構わない。

上は string となっている部分に Base64 エンコードした binary をぶちこむためのもので、ほぼこの書き方だけ使いまわせばよいと思う。上の例は ZIP だが、別に XML でも JSON でも要領は同じで問題ない。

ZIP ファイルは各自手作業で作ってほしい。サンプルは以下。

sudachi-installer-rb/spec/support at main · wtnabe/sudachi-installer-rb

More