[rails]carrierwaveを使った画像投稿APIをRspecでテストしてみる
rails 4.1.7, rspec 3.1.7 で確認 先日、
[[rails]carrierwaveを使って画像を保存するAPIサンプル](https://blog.hello-world.jp.net/ruby/2289/)
[[rails]base64エンコードされた画像をcarrierwaveに保存する](https://blog.hello-world.jp.net/ruby/2281/)
という記事を書きましたが、今回は作った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以下を自動読み込みするようにコメントアウトを解除します。
- factory_girlの基本設定を追加
# 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まだ色々分かっていないのでちゃんと勉強します。