TL;DR
CarrierWaveを使っていて、普段は Fog.mock!
しておきたいけれど、一部のテストケースでは Fog.unmock!
したいなら、以下のように書く
RSpec.configure do |config|
config.before do
Fog.mock!
Fog::Mock.reset
# ↓ Fog.unmock! しても期待通りに動かなくなるのを少し強引に調整
CarrierWave::Storage::Fog.connection_cache.clear
# ↓ ここは必要に応じて。 https://blog.engineyard.com/2011/mocking-fog-when-using-it-with-carrierwave を参考に。
Fog.credentials_path = Rails.root.join('config/test_fog_credentials.yml')
connection = Fog::Storage.new(:provider => 'AWS')
connection.directories.create(:key => ENV['AWS_S3_BUCKET'])
end
end
require 'rails_helper'
RSpec.describe Hoge, type: :model do
before do
# 実際にS3を叩きたい時はbeforeでunmockする
Fog.unmock!
end
end
原理とか説明的なの
ざっとした解説ですが、読みたい人はどうぞ。
なぜ Fog.unmock!
しただけでは動かないのか
以下のFog公式のIssueにあるとおり、Fog.mock!
/Fog.unmock!
はFog::Storage以下のクラスをイニシャライズするときにMockするかどうかを変更するだけで、既存のインスタンスには影響を及ぼさないようです。
CarrierWaveでは、かなり根っこの方でそのインスタンスを保持しているようでした。
なぜ上記のコードで動くようになるのか
ようするに、インスタンスを保持しているのが原因なので、それを一度削除し再生成させることで解決します。
重要なのは CarrierWave::Storage::Fog.connection_cache.clear
の部分です。
carrierwave-0.10.0/lib/carrierwave/storage/fog.rb
の 102行目あたりに以下のようなコードがあります
def connection
@connection ||= begin
options = credentials = uploader.fog_credentials
self.class.connection_cache[credentials] ||= ::Fog::Storage.new(options)
end
end
この中の self.class.connection_cache[credentials]
が既に存在していると、新しくイニシャライズされなくなってしまうので、一度このインスタンス変数をclearすることで、次に呼び出されたときも ::Fog::Storage.new(options)
が呼び出されるようにしているわけです。
もちろん、 before(:each)
でやらずに before(:all)
でやるなど、インスタンス生成コストを下げる方法はあるのですが、インスタンス生成のコストがかなり小さく無視出来るようなものだったので、最も単純な例をこの記事では挙げています。(実運用上もこの方法にしていますが、十分高速でした)
mockする前とmockした後の速度比較
非常に極端な例ですが、実際の例です。
ごく一部、aws-sdkでS3のAPIを直接叩いている部分があるプロダクトで、その部分のテストは実際にS3と通信しないと本当に動くことの確認が出来ないというものがありました。
この記事の方法をみつけるまでは、色んなことを諦めて全てのテストケースでS3と通信していました。
FactoryGirl側で、ユーザー情報やユーザーによる投稿の情報に画像が入っている(必須な場合もある)ので、ほぼ全てのケースで無意味にS3へ画像をアップロードしていました。
テストケースの数は399、RSpecの実行時間だけで38分程度かかっていたものが、2分半程度に短縮されました。明確にここがボトルネックだったんですね。ここまで寄与するとは…。