Edited at

Rails5でコントローラのテストをController specからRequest specに移行する

More than 1 year has passed since last update.


これはなに

RSpecを利用したコントローラの機能テストは、Rails4まではcontroller specで行われて来ました。しかしRails5からはrequest specで記述することが推奨され、assignsassert_templateの使用が非推奨となりました。

rails-controller-testinggemを使用すればassignsassert_templateを使うことはできますが、やはりrequest specへ移行することが望ましいと考えられています。


これから新しく作成する Rails アプリケーションについては、 rails-controller-testing gem を追加するのはおすすめしません。 Rails チームや RSpec コアチームとしては、代わりに request spec を書くことを推奨します。

RSpec 3.5 がリリースされました!


私は上記の話を聞いたとき、「controller specからrequest specへ移行しないとダメなんだな」と漠然と理解しましたが、そもそもrequest specを書いたことが無かったので両者の違いがわかりませんでした。

request specで色々調べてみましたが出てくるのは「WebAPIのテスト」や「Capybaraを使用したインテグレーションテスト」のサンプルが多く、私が求めている情報とは少し異なっていました。

しかしeverydayRailsに正に移行の手引きと言える投稿がありました。

Replacing RSpec controller specs, part 1: Request specs

Replacing RSpec controller specs, part 2: Feature specs

この内容を参考に、既存のcontroller specをrequest specに置き換えていき、そのうえでどのような点に注意すべきかを見ていきたいと思います。

以後内容において、そもそも理解が間違っている点や修正すべき点などありましたら、ご指摘頂けたら幸いです。


何をテストすべきか

controller specで行うべきテストは主に以下の項目です(でした)。



  • Webリクエストが成功したか

  • 正しいページにリダイレクトされたか

  • ユーザー認証が成功したか

  • レスポンスのテンプレートに正しいオブジェクトが保存されたか

  • ビューに表示されたメッセージは適切か

コントローラの機能テスト


request specでもテストすべき内容に大きな変更はありません。

リクエストに対するレスポンスが仕様に沿っているか検証します。

しかし「レスポンスのテンプレートに正しいオブジェクトが保存されたか」とあるような

コントローラの内部実装に関わる点はテストすべきではありません。1

これはrequest specはリクエスト/レスポンスにのみ関心を持つブラックボックステストであるからです。


複数のリクエスト、複数のコントローラ

request specのドキュメントを読むと、単一のリクエストを指定する他に、複数のコントローラや複数のセッションで複数のリクエストを指定できるとあり、そのサンプルも例示してあります。

https://relishapp.com/rspec/rspec-rails/v/3-7/docs/request-specs/request-spec

しかしrequest specにおいて、一つのテストケースでは単一のリクエストとレスポンスを処理することが望ましいように思えます。

コントローラを横断し、複数のリクエストを投げる必要があるケースはfeature spec2に委ねるべきです。

※そもそもドキュメントの内容がRSpec2.6の頃と同じだったり、サンプル中でassignsrender_templateを使用していることから内容が古いのではないかと思います。


なぜassignsassert_templateは使用すべきではないのか

これらのヘルパーを使用したテストはコントローラの実装に依存する為、脆弱であると考えられます。

コントローラのテストで注視すべき点はリクエストとレスポンスです。

そこにどのような変化があるかをテストすべきで、インスタンス変数の状態であったり、どのビューが呼び出されたかと言ったコントローラの実装に関わる点はテストすべきではないということです。


動作環境

rails 5.1.4

rspec-rails 3.7.2

今回使用したリポジトリ

https://github.com/t-kojima/controller-spec-to-request-spec


事前準備

Userモデルを作成し、ファクトリを定義しておきます。

rails generate scaffold User name:string email:string


app/models/user.rb

class User < ApplicationRecord

validates :name, presence: true
end


spec/factories/users.rb

FactoryBot.define do

factory :user do
name "hoge"
email "hoge@example.com"

trait :invalid do
name nil
end
end

factory :takashi, class: User do
name "Takashi"
email "takashi@example.com"
end

factory :satoshi, class: User do
name "Satoshi"
email "satoshi@example.com"
end
end



GET#index

まずはじめにindexページへアクセスするテストを見てみたいと思います。

index-001.PNG


controller_spec

describe UsersController, type: :controller do

describe 'GET #index' do
let(:users) { FactoryBot.create_list :user, 2 }

it 'リクエストが成功すること' do
get :index
expect(response.status).to eq 200
end

it 'indexテンプレートで表示されること' do
get :index
expect(response).to render_template :index
end

it '@usersが取得できていること' do
get :index
expect(assigns :users).to eq users
end
end
end


assignsrender_templateでエラーが出るので、request specに書き換えていきます。


request_spec

describe UsersController, type: :request do

describe 'GET #index' do
before do
FactoryBot.create :takashi
FactoryBot.create :satoshi
end

it 'リクエストが成功すること' do
get users_url
expect(response.status).to eq 200
end

it 'ユーザー名が表示されていること' do
get users_url
expect(response.body).to include "Takashi"
expect(response.body).to include "Satoshi"
end
end
end


get :indexget users_urlに置き換わります。(get '/users'のようにパスを直接記載してもOK)

controller specではindexメソッドを呼んでいますが、request specではエンドポイントを指定する形になり、controllerに依存しない形になっています。

また、テンプレートやインスタンス変数を評価する代わりにresponse.bodyを評価します。


GET#show


controller_spec

  describe 'GET #show' do

context 'ユーザーが存在する場合' do
let(:takashi) { FactoryBot.create :takashi }

it 'リクエストが成功すること' do
get :show, params: { id: takashi }
expect(response.status).to eq 200
end

it 'showテンプレートで表示されること' do
get :show, params: { id: takashi }
expect(response).to render_template :show
end

it '@userが取得できていること' do
get :show, params: { id: takashi }
expect(assigns :user).to eq takashi
end
end

context 'ユーザーが存在しない場合' do
subject { -> { get :show, params: { id: 1 } } }

it { is_expected.to raise_error ActiveRecord::RecordNotFound }
end
end



request_spec

  describe 'GET #show' do

context 'ユーザーが存在する場合' do
let(:takashi) { FactoryBot.create :takashi }

it 'リクエストが成功すること' do
get user_url takashi.id
expect(response.status).to eq 200
end

it 'ユーザー名が表示されていること' do
get user_url takashi.id
expect(response.body).to include 'Takashi'
end
end

context 'ユーザーが存在しない場合' do
subject { -> { get user_url 1 } }

it { is_expected.to raise_error ActiveRecord::RecordNotFound }
end
end


基本的にindexと同じです。

テンプレートやインスタンス変数ではなくresponse.bodyを評価します。


GET#new


controller_spec

  describe 'GET #new' do

it 'リクエストが成功すること' do
get :new
expect(response.status).to eq 200
end

it 'newテンプレートで表示されること' do
get :new
expect(response).to render_template :new
end

it '@userがnewされていること' do
get :new
expect(assigns :user).to_not be_nil
end
end



request_spec

  describe 'GET #new' do

it 'リクエストが成功すること' do
get new_user_url
expect(response.status).to eq 200
end
end

ここではリクエストの成否のみテストできればよいです。

ビューに関わるテストは「フォームにName,Emailを入力してUserを作成する」というfeature specのシナリオでカバーできます。


GET#edit


controller_spec

  describe 'GET #edit' do

let(:takashi) { FactoryBot.create :takashi }

it 'リクエストが成功すること' do
get :edit, params: { id: takashi }
expect(response.status).to eq 200
end

it 'editテンプレートで表示されること' do
get :edit, params: { id: takashi }
expect(response).to render_template :edit
end

it '@userが取得できていること' do
get :show, params: { id: takashi }
expect(assigns :user).to eq takashi
end
end



request_spec

  describe 'GET #edit' do

let(:takashi) { FactoryBot.create :takashi }

it 'リクエストが成功すること' do
get edit_user_url takashi
expect(response.status).to eq 200
end

it 'ユーザー名が表示されていること' do
get edit_user_url takashi
expect(response.body).to include 'Takashi'
end

it 'メールアドレスが表示されていること' do
get edit_user_url takashi
expect(response.body).to include 'takashi@example.com'
end
end


editもnewとあまり変わりはありません。name,emailがビューに表示されていることがテストできれば良いと思います。


POST#create

createアクションではパラメータが妥当な場合と不正な場合でテストを行います。


controller_spec

  describe 'POST #create' do

context 'パラメータが妥当な場合' do
it 'リクエストが成功すること' do
post :create, params: { user: FactoryBot.attributes_for(:user) }
expect(response.status).to eq 302
end

it 'ユーザーが登録されること' do
expect do
post :create, params: { user: FactoryBot.attributes_for(:user) }
end.to change(User, :count).by(1)
end

it 'リダイレクトすること' do
post :create, params: { user: FactoryBot.attributes_for(:user) }
expect(response).to redirect_to User.last
end
end

context 'パラメータが不正な場合' do
it 'リクエストが成功すること' do
post :create, params: { user: FactoryBot.attributes_for(:user, :invalid) }
expect(response.status).to eq 200
end

it 'ユーザーが登録されないこと' do
expect do
post :create, params: { user: FactoryBot.attributes_for(:user, :invalid) }
end.to_not change(User, :count)
end

it 'newテンプレートで表示されること' do
post :create, params: { user: FactoryBot.attributes_for(:user, :invalid) }
expect(response).to render_template :new
end

it 'エラーが表示されること' do
post :create, params: { user: FactoryBot.attributes_for(:user, :invalid) }
expect(assigns(:user).errors.any?).to be_truthy
end
end
end



request_spec

  describe 'POST #create' do

context 'パラメータが妥当な場合' do
it 'リクエストが成功すること' do
post users_url, params: { user: FactoryBot.attributes_for(:user) }
expect(response.status).to eq 302
end

it 'ユーザーが登録されること' do
expect do
post users_url, params: { user: FactoryBot.attributes_for(:user) }
end.to change(User, :count).by(1)
end

it 'リダイレクトすること' do
post users_url, params: { user: FactoryBot.attributes_for(:user) }
expect(response).to redirect_to User.last
end
end

context 'パラメータが不正な場合' do
it 'リクエストが成功すること' do
post users_url, params: { user: FactoryBot.attributes_for(:user, :invalid) }
expect(response.status).to eq 200
end

it 'ユーザーが登録されないこと' do
expect do
post users_url, params: { user: FactoryBot.attributes_for(:user, :invalid) }
end.to_not change(User, :count)
end

it 'エラーが表示されること' do
post users_url, params: { user: FactoryBot.attributes_for(:user, :invalid) }
expect(response.body).to include 'prohibited this user from being saved'
end
end
end


パラメータが不正でエラーが発生した場合、controller specでは@user.errorsを直接参照していますが、request specでは'prohibited this user from being saved'がビューに表示されるかどうかをテストしています。

create-001.PNG


PUT#update

updateアクションでもcreateアクションと同様にパラメータが妥当な場合と不正な場合でテストを行います。


controller_spec

  describe 'PUT #update' do

let(:takashi) { FactoryBot.create :takashi }

context 'パラメータが妥当な場合' do
it 'リクエストが成功すること' do
put :update, params: { id: takashi, user: FactoryBot.attributes_for(:satoshi) }
expect(response.status).to eq 302
end

it 'ユーザー名が更新されること' do
expect do
put :update, params: { id: takashi, user: FactoryBot.attributes_for(:satoshi) }
end.to change { User.find(takashi.id).name }.from('Takashi').to('Satoshi')
end

it 'リダイレクトすること' do
put :update, params: { id: takashi, user: FactoryBot.attributes_for(:satoshi) }
expect(response).to redirect_to User.last
end
end

context 'パラメータが不正な場合' do
it 'リクエストが成功すること' do
put :update, params: { id: takashi, user: FactoryBot.attributes_for(:user, :invalid) }
expect(response.status).to eq 200
end

it 'ユーザー名が変更されないこと' do
expect do
put :update, params: { id: takashi, user: FactoryBot.attributes_for(:user, :invalid) }
end.to_not change(User.find(takashi.id), :name)
end

it 'editテンプレートで表示されること' do
put :update, params: { id: takashi, user: FactoryBot.attributes_for(:user, :invalid) }
expect(response).to render_template :edit
end

it 'エラーが表示されること' do
put :update, params: { id: takashi, user: FactoryBot.attributes_for(:user, :invalid) }
expect(assigns(:user).errors.any?).to be_truthy
end
end
end


putメソッドではcontroller specとrequest specで若干異なります。(request specではparamsにidを含めない)


request_spec

  describe 'PUT #update' do

let(:takashi) { FactoryBot.create :takashi }

context 'パラメータが妥当な場合' do
it 'リクエストが成功すること' do
put user_url takashi, params: { user: FactoryBot.attributes_for(:satoshi) }
expect(response.status).to eq 302
end

it 'ユーザー名が更新されること' do
expect do
put user_url takashi, params: { user: FactoryBot.attributes_for(:satoshi) }
end.to change { User.find(takashi.id).name }.from('Takashi').to('Satoshi')
end

it 'リダイレクトすること' do
put user_url takashi, params: { user: FactoryBot.attributes_for(:satoshi) }
expect(response).to redirect_to User.last
end
end

context 'パラメータが不正な場合' do
it 'リクエストが成功すること' do
put user_url takashi, params: { user: FactoryBot.attributes_for(:user, :invalid) }
expect(response.status).to eq 200
end

it 'ユーザー名が変更されないこと' do
expect do
put user_url takashi, params: { user: FactoryBot.attributes_for(:user, :invalid) }
end.to_not change(User.find(takashi.id), :name)
end

it 'エラーが表示されること' do
put user_url takashi, params: { user: FactoryBot.attributes_for(:user, :invalid) }
expect(response.body).to include 'prohibited this user from being saved'
end
end
end



DELETE#destroy

最後はdestroyアクションです。

updateアクションと同様にdeleteメソッドの引数のみ注意して下さい。


controller_spec

  describe 'DELETE #destroy' do

let!(:user) { FactoryBot.create :user }

it 'リクエストが成功すること' do
delete :destroy, params: { id: user }
expect(response.status).to eq 302
end

it 'ユーザーが削除されること' do
expect do
delete :destroy, params: { id: user }
end.to change(User, :count).by(-1)
end

it 'ユーザー一覧にリダイレクトすること' do
delete :destroy, params: { id: user }
expect(response).to redirect_to(users_url)
end
end



request_spec

  describe 'DELETE #destroy' do

let!(:user) { FactoryBot.create :user }

it 'リクエストが成功すること' do
delete user_url user
expect(response.status).to eq 302
end

it 'ユーザーが削除されること' do
expect do
delete user_url user
end.to change(User, :count).by(-1)
end

it 'ユーザー一覧にリダイレクトすること' do
delete user_url user
expect(response).to redirect_to(users_url)
end
end



JSON API

JSON APIについては触れてきませんでしたが、最後に少しだけ触れたいと思います。

controller specではresponse.bodyをテストすることができません。(render_viewを呼ばない限りresponse.body""になる)その為JSON APIのテストはrequest specで行うのが適切です。

indexアクションの場合は以下のような感じになるはずです。3


request_spec

  describe 'GET /users.json' do

let(:headers) do
{ 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
end
before do
FactoryBot.create :takashi
FactoryBot.create :satoshi
end

it 'リクエストが成功すること' do
get users_url, headers: headers
expect(response.status).to eq 200
end

it 'ユーザー一覧が取得できていること' do
get users_url, headers: headers
expect(response.body).to have_json_size 2
end
end



さいごに

scaffoldで生成されるアクションについては一通り置き換えを行ってみましたが、ほとんど変わらないような印象を持たれたのではないでしょうか。

参考元でも構造と構文に大きな違いは無いとしていますが、request specのアプローチにはより将来性があると結んでいます。

テストを書いている途中に「これはrequest spec?feature specで書くべき?」と悩む所がいくつかあったので、次はfeature specを書いてみたいと思います。


参考

RSpecでRequest Describer

RequestのRSpecを実装する

Changes to test controllers in Rails 5

Replacing RSpec controller specs, part 1: Request specs

Replacing RSpec controller specs, part 2: Feature specs





  1. 後述:なぜassignsとassert_templateは使用すべきではないのか 



  2. Rspec3.7からはSystemSpecが推奨のようです 



  3. json_specを使っています