[rails]carrierwaveを使った画像投稿APIをRspecでテストしてみる


rails 4.1.7, rspec 3.1.7 で確認

先日、

という記事を書きましたが、今回は作ったAPIをRSpecでテストしてみたいと思います。

作成したサンプルのリポジトリは以下になります。

RSpecの準備

  • Gemfile追加
group :development, :test do
  gem 'pry-rails'
  gem 'rspec-rails'
  gem 'factory_girl_rails'
end

pry-railsはテスト時のデバッグ用に追加しています。

  • bundle install
bundle install --path vendor/bundler
  • rspec初期設定
./bin/rails generate rspec:install

spec以下にhelper等が追加されます

  • spec/rails_helper.rbを編集
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

spec/support以下を自動読み込みするようにコメントアウトを解除します。

# spec/support/factory_girl.rb

RSpec.configure do |config|
  config.include FactoryGirl::Syntax::Methods
  config.before(:all) do
      FactoryGirl.reload
  end
end
  • user factoryの作成

FactoryGirlを使ってテストユーザを作成できるようにしておきます。

# spec/factories/users.rb

include ActionDispatch::TestProcess

FactoryGirl.define do
  factory :user do
    name 'sample'
    profile_image { fixture_file_upload("spec/fixtures/img/sample.png", 'image/png') }
  end
end

fixture_file_uploadを使うことでprofile_imageをアップロード済みのuserをつくることができます。

あらかじめ spec/fixtures/img/ 以下に sample.jpg を追加しておきます。

今回は、「Placehold.jp」さんのダミー画像を利用させていただきました。

curl -o spec/fixtures/img/sample.png http://placehold.jp/100x100.png?text=sample

これでテスト内に create(:user)とすればnameが「sample」のテストユーザを作成することができます。

create(:user, { name: ‘test’ }) という風に第二引数にパラメータを渡すと、指定した状態のテストユーザを作成することができます。

Request specを書く

Rails でつくる API のテストの書き方(RSpec + FactoryGirl)」および「RESTful Web API 開発をささえる Garage」の記事を参考にさせていただきました。

APIのテストはRequest specで書くのが良いとのことなので spec/requests ディレクトリを作成し、 app/controllers/user_controller.rb に対するrequest specを作成します。

まずはrequest用のhelperを追加し、paramsやenvをローカル変数として扱えるようにしたり、ファイルパスからbase64エンコードの結果を返すヘルパーメソッドが使えるようにしておきます。

# spec/support/request_helper.rb

require 'active_support/concern'

module RequestHelper
  extend ActiveSupport::Concern

  included do
    let(:params) { {} }

    let(:env) do
      {
        accept: 'application/json',
      }
    end
  end

  private

  def base64_image_param(path)
    'data:image/png;base64,' + Base64.strict_encode64(File.new(path).read)
  end
end

次に、APIテストの追加

# spec/requests/user_spec.rb

require 'rails_helper'

RSpec.describe 'coordinate_upload', type: :request do
  include RequestHelper
  include ActionDispatch::TestProcess

  describe 'POST /user/update' do
    let(:update_structure) do
      {
        'name' => a_kind_of(String),
        'profile_url' => a_kind_of(String),
      }
    end

    let(:path) { '/user/update' }

    context 'パラメータが正しいとき' do
      before do
        params[:name] = 'upload-man'
        params[:profile] = fixture_file_upload("img/sample.png", 'image/png')
        env['Content-Type'] = 'multipart/form-data'
      end

      it '200 が返ってくる' do
        post path, params, env
        expect(response).to have_http_status(200)
      end

      it '写真をアップロードする' do
        post path, params, env
        json = JSON.parse(response.body)

        expect(json).to match(update_structure)
      end

      it 'User が 1 増える' do
        expect {
          post path, params, env
        }.to change(User, :count).by(1)
      end
    end
  end

  describe 'POST /user/update_base64' do
    let(:update_base64_structure) do
      {
        'name' => a_kind_of(String),
        'profile_url' => a_kind_of(String),
      }
    end

    let(:path) { '/user/update_base64' }

    context 'パラメータが正しいとき' do
      before do
        params[:name] = 'upload-man'
        params[:profile_base64] = base64_image_param("#{Rails.root}/spec/fixtures/img/sample.png")
        env['Content-Type'] = 'application/json'
      end

      it '200 が返ってくる' do
        post path, params.to_json, env
        expect(response).to have_http_status(200)
      end

      it '写真をアップロードする' do
        post path, params.to_json, env
        json = JSON.parse(response.body)

        expect(json).to match(update_base64_structure)
      end

      it 'User が 1 増える' do
        expect {
          post path, params.to_json, env
        }.to change(User, :count).by(1)
      end
    end
  end
end

(本来はpostでリソース追加を行った場合は201を返すべきですが、今回はリソース追加も更新も200を返すようにしています)

「/user/update」と「/user/update_base64」の正常系のテストを書いて、rspec実行

bundle exec rspec spec/requests/user_spec.rb

これで正常系のテストを実施することができました。

異常系のテストも追加してみます。

# spec/requests/user_spec.rb

require 'rails_helper'

RSpec.describe 'coordinate_upload', type: :request do
  include RequestHelper
  include ActionDispatch::TestProcess

  describe 'POST /user/update' do
    let(:update_structure) do
      {
        'name' => a_kind_of(String),
        'profile_url' => a_kind_of(String),
      }
    end

    let(:path) { '/user/update' }

    context 'パラメータが正しいとき' do
      before do
        params[:name] = 'upload-man'
        params[:profile] = fixture_file_upload("img/sample.png", 'image/png')
        env['Content-Type'] = 'multipart/form-data'
      end

      it '200 が返ってくる' do
        post path, params, env
        expect(response).to have_http_status(200)
      end

      it '写真をアップロードする' do
        post path, params, env
        json = JSON.parse(response.body)

        expect(json).to match(update_structure)
      end

      it 'User が 1 増える' do
        expect {
          post path, params, env
        }.to change(User, :count).by(1)
      end
    end

    context 'nameが入っていないとき' do
      before do
        params[:name] = nil
        params[:profile] = fixture_file_upload("img/sample.png", 'image/png')
        env['Content-Type'] = 'multipart/form-data'
      end

      it '400 が返ってくる' do
        post path, params, env
        expect(response).to have_http_status(400)
      end

      it 'User が増減しない' do
        expect {
          post path, params
        }.not_to change(User, :count)
      end
    end

    context 'profileが入っていないとき' do
      before do
        params[:name] = 'upload-man'
        params[:profile] = nil
        env['Content-Type'] = 'multipart/form-data'
      end

      it '400 が返ってくる' do
        post path, params, env
        expect(response).to have_http_status(400)
      end

      it 'invalid params が返ってくる' do
        post path, params, env
        json = JSON.parse(response.body)

        expect(json['message']).to eq('invalid params')
      end

      it 'User が増減しない' do
        expect {
          post path, params
        }.not_to change(User, :count)
      end
    end
  end

  describe 'POST /user/update_base64' do
    let(:update_base64_structure) do
      {
        'name' => a_kind_of(String),
        'profile_url' => a_kind_of(String),
      }
    end

    let(:path) { '/user/update_base64' }

    context 'パラメータが正しいとき' do
      before do
        params[:name] = 'upload-man'
        params[:profile_base64] = base64_image_param("#{Rails.root}/spec/fixtures/img/sample.png")
        env['Content-Type'] = 'application/json'
      end

      it '200 が返ってくる' do
        post path, params.to_json, env
        expect(response).to have_http_status(200)
      end

      it '写真をアップロードする' do
        post path, params.to_json, env
        json = JSON.parse(response.body)

        expect(json).to match(update_base64_structure)
      end

     it 'User が 1 増える' do
        expect {
          post path, params.to_json, env
        }.to change(User, :count).by(1)
      end
    end

    context 'nameが入っていないとき' do
      before do
        params[:name] = nil
        params[:profile_base64] = base64_image_param("#{Rails.root}/spec/fixtures/img/sample.png")
        env['Content-Type'] = 'multipart/form-data'
      end

      it '400 が返ってくる' do
        post path, params.to_json, env
        expect(response).to have_http_status(400)
      end

      it 'User が増減しない' do
        expect {
          post path, params.to_json, env
        }.not_to change(User, :count)
      end
    end

    context 'profile_base64が入っていないとき' do
      before do
        params[:name] = 'upload-man'
        params[:profile_base64] = nil
        env['Content-Type'] = 'application/json'
      end

      it '400 が返ってくる' do
        post path, params.to_json, env
        expect(response).to have_http_status(400)
      end

      it 'invalid params が返ってくる' do
        post path, params.to_json, env
        json = JSON.parse(response.body)

        expect(json['message']).to eq('invalid params')
      end

      it 'User が増減しない' do
        expect {
          post path, params.to_json, env
        }.not_to change(User, :count)
      end
    end
  end
end

現状だと必要なパラメータ不足でArgumentErrorになった場合、そのままhtmlが返ってしまうので上記テストは失敗します。

エラー時もjsonレスポンスを返すように修正します。

application_controller.rbにエラー処理を追加。

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session

  rescue_from Exception,      with: :render_500
  rescue_from ArgumentError,  with: :render_400

  private

  def render_400(e = nil)
    error_handler(400, e ? e.message : nil)
  end

  def render_500(e = nil)
    if e
      logger.error "Rendering 500 with exception: #{e.message}"
    end
    error_handler(500)
  end

  def error_handler(code, response = '')
    render json: {
      message: response
    }, status: code
  end
end

これでArgumentError発生時もjsonレスポンスが返るようになりました。

と、こんな風に今回はcarrierwaveを使った画像投稿APIをRSpecでテストしてみました。

テストは最初の準備が結構手間ですがベースができると後は必要な部分のテストを書いていく流れができるので、この調子を維持していきたいです。

テストがあると安心感が増しますね。

RSpecまだ色々分かっていないのでちゃんと勉強します。

参考