Amazon S3へのファイルアップロードを使った実装のテストの際に利用したのでメモ。
Fake S3はAmazon s3をシュミレートしてくれるサーバで、glintは空いているポートを見つけてサーバを立ち上げてくれる。
参考ページ
参考にさせてもらったページ。
基本的にはほとんど同じですが、glintを使った処理のconfig.after :suite
のところで時々エラーが出てしまったので、多少改変して利用しました。
前提条件
とりあえず利用している環境情報
- Rails 4.2.7
- Rspec 3系
インストール
テストのときだけFake S3を起動したいので、Gemfileのgroup :test
に追加します。
gem 'aws-sdk', '~> 2'
group :test do
gem 'fakes3'
gem 'glint'
end
そして、bundle install
を実行。
使う
AWS S3への通常の接続設定
Herokuのドキュメント Direct to S3 Image Uploads in Rails を参考に、config/initializers/aws.rb
に定義していました。
基本的には環境変数に定義するようにしてます。
Aws.config.update({
region: 'ap-northeast-1',
credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']),
})
S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['S3_BUCKET'])
Fake S3の起動
Rspecのconfig.before :suite
のタイミングで起動します。(つまりテスト実行時に1度だけ実行する)
サーバが止まったタイミングで一時フォルダに作られたファイルを削除するようにしています。(server.on_stopped
)
参考URLでは、config.after :suite
でファイル削除をしていて、それでも動くと思っていたのですが、なぜかFake S3が起動し終わる前にconfig.after :suite
ブロックが評価されてしまい、一時フォルダが見つからないというエラーで落ちたり落ちなかったりと不安定だったので、やめました。
require 'tmpdir'
require 'glint'
RSpec.configure do |config|
config.before :suite do
rootdir = Dir.mktmpdir
server = Glint::Server.new(nil, signals: [:INT]) do |port|
exec "bundle exec fakes3 -p #{port} -r #{rootdir}", err: '/dev/null'
end
# サーバが止まったタイミングでファイルを消す
server.on_stopped = -> {
if Dir.exists? Glint::Server.info[:fakes3][:root]
FileUtils.remove_entry_secure(Glint::Server.info[:fakes3][:root])
end
}
server.start
Glint::Server.info[:fakes3] = {
address: "127.0.0.1:#{server.port}",
root: rootdir
}
end
end
Fake S3の起動情報をGlint::Server.info
というグローバル領域に保存しておきます。
AWS SDKを使ってFake S3へ接続
接続はエンドポイントをローカルにする必要があります。接続はconfig/initializers/aws.rb
で、S3_BUCKET
という定数を作っているので、それをスタブで置き換えます。
RSpec.configure do |config|
# 他の設定は省略
# ...
config.before :each do |example|
# S3_BUCKETをfakes3に接続したスタブに置き換える
s3_client = Aws::S3::Client.new(
access_key_id: "dummy_aws_access_key_id",
secret_access_key: "dummy_aws_secret_access_key",
region: "ap-northeast-1",
endpoint: "http://#{Glint::Server.info[:fakes3][:address]}/",
force_path_style: true)
Aws::S3::Resource.new(client: s3_client).create_bucket(
bucket: "dummy_bucket_name"
)
stub_const("S3_BUCKET", Aws::S3::Resource.new(client: s3_client).bucket("dummy_bucket_name"))
end
# 他の設定は省略
# ...
end
テストを書いてみる
とりあえず適当にテストを書いてみます。
# == Schema Information
#
# Table name: foos
#
# id :integer not null, primary key
# s3_object_key :text
# created_at :datetime
# updated_at :datetime
#
describe Foo, type: :model do
let(:s3_object) {
S3_BUCKET.put_object(
acl: 'private',
key: 'spec/files/test.png',
body: File.open('spec/files/test.png')
)
}
subject { Foo.new(s3_object_key: s3_object.key) }
it '保存できること' do
expect(subject).to be_valid
expect(subject.save).to be_truty
expect(S3_BUCKET.object(subject.s3_object_key).presigned_url(:get, expires_in: 300)).not_to be_blank
end
end
感想
テストのときだけFake S3を自動起動し、テストが終了したらファイルを自動削除してくれるようになったのでよかったです。また、S3_BUCKETを毎回スタブで置き換えているので、本物のS3につながることもないという安心感があります。
ただ、全然関係ないテストのときにもS3_BUCKETをスタブに置き換える処理が挟まれるから、微妙にオーバーヘッドあるかなーという気はしてます…。