fakes3とglintでS3を使った実装のテストを行う

Amazon S3へのファイルアップロードを使った実装のテストの際に利用したのでメモ。

- https://github.com/jubos/fake-s3
- https://github.com/kentaro/glint

Fake S3はAmazon s3をシュミレートしてくれるサーバで、glintは空いているポートを見つけてサーバを立ち上げてくれる。

## 参考ページ

参考にさせてもらったページ。

- [S3 を使った実装のテストを fake-s3 を使って書いてみる](http://koshigoeb.hateblo.jp/entry/2015/03/21/122904)

基本的にはほとんど同じですが、glintを使った処理の`config.after :suite`のところで時々エラーが出てしまったので、多少改変して利用しました。

## 前提条件

とりあえず利用している環境情報

- Rails 4.2.7
- Rspec 3系

## インストール

テストのときだけFake S3を起動したいので、Gemfileの`group :test`に追加します。

```rb:Gemfile
gem 'aws-sdk', '~> 2'

group :test do
  gem 'fakes3'
  gem 'glint'
end
```

そして、`bundle install`を実行。

## 使う

### AWS S3への通常の接続設定

Herokuのドキュメント [Direct to S3 Image Uploads in Rails](https://devcenter.heroku.com/articles/direct-to-s3-image-uploads-in-rails) を参考に、`config/initializers/aws.rb`に定義していました。
基本的には環境変数に定義するようにしてます。

```rb: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`ブロックが評価されてしまい、一時フォルダが見つからないというエラーで落ちたり落ちなかったりと不安定だったので、やめました。

```rb:spec/support/fakes3.rb
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`という定数を作っているので、それをスタブで置き換えます。

```rb:rails_helper.rb
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
```

## テストを書いてみる

とりあえず適当にテストを書いてみます。

```rb:spec/models/foo.rb
# == 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をスタブに置き換える処理が挟まれるから、微妙にオーバーヘッドあるかなーという気はしてます…。