はじめに
Rails 5を境に、コントローラのテスト方針が大きく変わりました。従来のcontroller specで使われていたassignsやassert_templateは非推奨となり、request specへの移行(追加)が推奨されています。
rails-controller-testing gemを導入すれば旧来の書き方も可能ですが、RSpecコアチームとRailsチームは共にrequest specを推奨しています。
本記事では、controller specからrequest specへの移行(追加)方法を、具体的なコード例とともに解説します。
なぜrequest specでリファクタリングの信頼性が上がるのか
結論から言うと、request specは「実装の詳細」ではなく「振る舞い」をテストするからです。
controller specの問題点
controller specでは以下のようなテストを書いていました。
# コントローラの内部実装に依存したテスト
it '@usersにユーザー一覧が格納される' do
get :index
expect(assigns(:users)).to eq users
end
it 'indexテンプレートを描画する' do
get :index
expect(response).to render_template :index
end
これらのテストはコントローラの内部実装に強く依存しています。例えば:
- インスタンス変数名を
@usersから@membersに変更 → テスト失敗 - ビューファイルを
index.html.erbからlist.html.erbに変更 → テスト失敗 - Service Objectにロジックを切り出して変数の受け渡し方を変更 → テスト失敗
機能としては正しく動作しているのに、テストが壊れるという状況が頻発します。
これがリファクタリングを躊躇させる原因です。「テストが壊れるかもしれない」という恐怖が、コードの改善を妨げてしまうのです。
request specが解決すること
request specは「ユーザーから見た振る舞い」だけをテストします。
# 振る舞いをテストする
it '登録済みユーザーの名前がすべて出力される' do
get users_path
users.each do |user|
expect(response.body).to include(user.name)
end
end
このテストが検証しているのは「ユーザー一覧ページにアクセスしたら、登録済みユーザーの名前が表示される」という仕様です。
内部でどのような変数名を使っていようが、どのテンプレートを使っていようが、Service Objectを使っていようが関係ありません。仕様通りに動いていればテストは通るのです。
リファクタリング時の安心感
| シナリオ | controller spec | request spec |
|---|---|---|
変数名を@users→@membersに変更 |
❌ 失敗 | ✅ 成功 |
| Decoratorパターンを導入 | ❌ 失敗の可能性 | ✅ 成功 |
| Service Objectへのロジック切り出し | ❌ 失敗の可能性 | ✅ 成功 |
| N+1解消のためのクエリ最適化 | ✅ 成功 | ✅ 成功 |
| 表示内容の変更 | ✅ 成功 | ❌ 失敗(正しい挙動) |
最後の行がポイントです。表示内容を変更したら、request specは失敗します。これは正しい失敗です。仕様が変わったのですから、テストも更新すべきなのです。
バグ改修時の信頼性
バグ改修においても、request specの方が信頼性が高くなります。
controller specの場合:
- インスタンス変数に正しい値が入っていることは確認できる
- しかし、実際にユーザーに正しく表示されているかは保証されない
- ビューのバグは検出できない
request specの場合:
- 実際のHTTPリクエスト〜レスポンスの流れをテスト
- ユーザーが目にする最終的な出力を検証
- ビューを含めた統合的な動作確認が可能
つまり、request specは**「ユーザーが体験する挙動」をそのままテストしている**ため、バグ改修後の確認として信頼できるのです。
テストの考え方の違い
controller spec(従来)
- コントローラのメソッドを直接呼び出す
- インスタンス変数(
@userなど)の状態を検証 - 描画されるテンプレートを検証
- ホワイトボックステスト:内部実装を知っている前提
request spec(現在)
- HTTPリクエストをエンドポイントに送信
- レスポンスのステータスコードやボディを検証
- コントローラ内部には関与しない
- ブラックボックステスト:内部実装を知らなくても書ける
動作環境
- Rails 5.1.4
- rspec-rails 3.7.2
モデルとファクトリの準備
class User < ApplicationRecord
validates :name, presence: true
end
FactoryBot.define do
factory :user do
sequence(:name) { |n| "テストユーザー#{n}" }
sequence(:email) { |n| "test#{n}@sample.org" }
trait :invalid do
name { nil }
end
end
factory :admin_user, class: User do
sequence(:name) { |n| "管理者#{n}" }
sequence(:email) { |n| "admin#{n}@sample.org" }
end
factory :general_user, class: User do
sequence(:name) { |n| "一般ユーザー#{n}" }
sequence(:email) { |n| "member#{n}@sample.org" }
end
end
各アクションの書き換え例
一覧表示(index)
Before: controller spec
describe UsersController, type: :controller do
describe 'GET #index' do
let(:users) { FactoryBot.create_list :user, 2 }
it 'ステータス200を返す' 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
After: request spec
describe 'ユーザー一覧API' do
let!(:users) { FactoryBot.create_list(:general_user, 3) }
describe 'GET /users' do
subject { get users_path }
it 'HTTPステータス200が返却される' do
subject
expect(response).to have_http_status(:ok)
end
it '登録済みユーザーの名前がすべて出力される' do
subject
users.each do |user|
expect(response.body).to include(user.name)
end
end
end
end
ポイント
-
get :index→get users_path(URLヘルパーまたはパス文字列を使用) -
assigns(:users)→response.bodyの検証に置き換え -
render_template→ レスポンスボディの内容で間接的に確認 -
subjectを活用してリクエスト処理を共通化
詳細表示(show)
describe 'ユーザー詳細API' do
describe 'GET /users/:id' do
context '存在するユーザーの場合' do
let(:target_user) { FactoryBot.create(:general_user) }
subject { get user_path(target_user) }
it 'HTTPステータス200が返却される' do
subject
expect(response).to have_http_status(:ok)
end
it 'ユーザー名がレスポンスに含まれる' do
subject
expect(response.body).to include(target_user.name)
end
end
context '存在しないユーザーの場合' do
it 'RecordNotFoundが発生する' do
expect { get user_path(id: 9999) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end
新規作成フォーム(new)
describe 'ユーザー新規作成フォーム' do
describe 'GET /users/new' do
it 'HTTPステータス200が返却される' do
get new_user_path
expect(response).to have_http_status(:ok)
end
end
end
フォーム自体の動作検証はfeature specに任せるのが適切です。ここではエンドポイントが正常に応答することだけを確認します。
編集フォーム(edit)
describe 'ユーザー編集フォーム' do
let(:target_user) { FactoryBot.create(:general_user) }
describe 'GET /users/:id/edit' do
subject { get edit_user_path(target_user) }
it 'HTTPステータス200が返却される' do
subject
expect(response).to have_http_status(:ok)
end
it '既存の値がレスポンスに含まれる' do
subject
expect(response.body).to include(target_user.name)
expect(response.body).to include(target_user.email)
end
end
end
登録処理(create)
describe 'ユーザー登録API' do
describe 'POST /users' do
context '有効なパラメータの場合' do
let(:valid_attributes) { FactoryBot.attributes_for(:user) }
subject { post users_path, params: { user: valid_attributes } }
it 'HTTPステータス302が返却される' do
subject
expect(response).to have_http_status(:found)
end
it 'ユーザーが1件増える' do
expect { subject }.to change(User, :count).by(1)
end
it '作成したユーザーの詳細ページへリダイレクトする' do
subject
expect(response).to redirect_to(User.last)
end
end
context '無効なパラメータの場合' do
let(:invalid_attributes) { FactoryBot.attributes_for(:user, :invalid) }
subject { post users_path, params: { user: invalid_attributes } }
it 'HTTPステータス200が返却される(フォーム再表示)' do
subject
expect(response).to have_http_status(:ok)
end
it 'ユーザーが増えない' do
expect { subject }.not_to change(User, :count)
end
it 'エラーメッセージがレスポンスに含まれる' do
subject
expect(response.body).to include('prohibited this user from being saved')
end
end
end
end
更新処理(update)
describe 'ユーザー更新API' do
let(:target_user) { FactoryBot.create(:general_user) }
describe 'PUT /users/:id' do
context '有効なパラメータの場合' do
let(:new_attributes) { { name: '更新後の名前', email: 'updated@sample.org' } }
subject { put user_path(target_user), params: { user: new_attributes } }
it 'HTTPステータス302が返却される' do
subject
expect(response).to have_http_status(:found)
end
it 'ユーザー名が更新される' do
expect { subject }.to change { target_user.reload.name }.to('更新後の名前')
end
it '更新したユーザーの詳細ページへリダイレクトする' do
subject
expect(response).to redirect_to(target_user)
end
end
context '無効なパラメータの場合' do
let(:invalid_attributes) { { name: nil } }
subject { put user_path(target_user), params: { user: invalid_attributes } }
it 'HTTPステータス200が返却される' do
subject
expect(response).to have_http_status(:ok)
end
it 'ユーザー名が変更されない' do
expect { subject }.not_to change { target_user.reload.name }
end
it 'エラーメッセージがレスポンスに含まれる' do
subject
expect(response.body).to include('prohibited this user from being saved')
end
end
end
end
request specではuser_path(target_user)のようにURLヘルパーにオブジェクトを渡します。controller specのようにparams: { id: target_user }とする必要はありません。
削除処理(destroy)
describe 'ユーザー削除API' do
let!(:target_user) { FactoryBot.create(:user) }
describe 'DELETE /users/:id' do
subject { delete user_path(target_user) }
it 'HTTPステータス302が返却される' do
subject
expect(response).to have_http_status(:found)
end
it 'ユーザーが1件減る' do
expect { subject }.to change(User, :count).by(-1)
end
it '一覧ページへリダイレクトする' do
subject
expect(response).to redirect_to(users_path)
end
end
end
JSON APIのテスト
JSON APIのテストはrequest specの得意分野です。controller specではrender_viewsを呼ばない限りresponse.bodyが空になりますが、request specでは自然にテストできます。
describe 'ユーザー一覧JSON API' do
let(:json_headers) do
{
'Content-Type' => 'application/json',
'Accept' => 'application/json'
}
end
let!(:users) { FactoryBot.create_list(:general_user, 3) }
describe 'GET /users.json' do
subject { get users_path, headers: json_headers }
it 'HTTPステータス200が返却される' do
subject
expect(response).to have_http_status(:ok)
end
it '登録済みユーザー件数分のデータが返却される' do
subject
json_response = JSON.parse(response.body)
expect(json_response.length).to eq(3)
end
end
end
request specとfeature specの使い分け
request specは単一のリクエスト/レスポンスに焦点を当てます。以下のようなケースはfeature specで扱うべきです。
- 複数のコントローラをまたぐ操作フロー
- ユーザーの視点からのシナリオテスト(「ログインしてユーザーを作成する」など)
- JavaScriptを含むインタラクション
まとめ
controller specからrequest specへの移行(追加)は、見た目ほど大きな変更ではありません。主な違いは以下の点です。
| 観点 | controller spec | request spec |
|---|---|---|
| リクエスト | get :index |
get users_path |
| パラメータ | params: { id: user } |
URLヘルパーに含める |
| 検証対象 |
assigns, render_template
|
response.body, response.status
|
| 性質 | ホワイトボックス | ブラックボックス |
request specに移行することで得られるメリットをまとめます:
- リファクタリングへの耐性 - 内部実装を変更してもテストが壊れにくい
- バグ改修の信頼性向上 - ユーザー体験に近い形で動作確認できる
- テストの意図が明確 - 「何をテストしているか」が仕様ベースで理解しやすい
- メンテナンスコスト削減 - 実装変更のたびにテストを修正する必要がない
新規プロジェクトでは迷わずrequest specを選択しましょう。既存プロジェクトでも、新しいテストからrequest specで書き始めることをお勧めします。
参考リンク