こんにちは、Railsエンジニアにょけんです。

RailsのテストとしてRSpecを使っているけど、コントローラーのテストって何すればいいか分かんない…
こんなお悩みに答えます。
Contents
コントローラーのSpecは、あんまり重要じゃない
まず1番肝心なことをお伝えすると、コントローラーのSpecは優先度・重要度が低いです。
なぜなら、アクセスを行った際にアプリがどんな動きをするかはFeatureSpecて(ブラウザでの動作をシミュレートするテスト)で検証するのがメインだから。
そのため、コントローラのSpecは、基本的にアクセス制御(不正なユーザー登録をはじくとか)が機能しているかを確認する程度であることが多いです。

例えば、コントローラのテストでユーザーの作成が成功したとしても、「ブラウザにユーザー作成ボタンがなかった」みたいな凡ミスがあるかもしれませんよね?
こうしたミスを防ぐ意味でも、FeatureSpecで実際の動作を検証することが多いよ!
こうしたミスを防ぐ意味でも、FeatureSpecで実際の動作を検証することが多いよ!
そんなわけで、今回はアクセス制御の部分に焦点を当てていきます。
【今回テストを書くアプリの想定】
- Userが複数のTaskを持っている(User:Task = 1:多)
- ファクトリを使用している

ファクトリを使用していない方は、「@user = FactoryBot.create(:user) 」を「@user = User.new(引数) 」みたいに読み替えてください。
indexアクションのSpec
indexアクションの動き
- 未ログインの場合は、ログインページにリダイレクト
describe "#index" do # 未ログインのユーザーの場合 context "as a user not to login" do # indexアクションのレスポンスのステータスが「302(失敗)」であることを確認 it "returns a 302 response" do get :index expect(response).to have_http_status "302" end # indexアクションを行うとサインインページに遷移することを確認 it "redirects to the sign-in page" do get :index expect(response).to redirect_to "/users/sign_in" end end end
showアクションのSpec
showアクションで確認すること
- 未ログインのユーザーがアクセスすると、ルートページにリダイレクトされる
describe "#show" do # 別ユーザーのTaskを参照しようとした場合 context "if logged in user shows another user" do before do # 各example(it〜end)の前に「@user」「other_user」「(other_userの)@task」を作成 @user = FactoryBot.create(:user) other_user = FactoryBot.create(:user) @task = FactoryBot.create(:task, owner: other_user) end # ルートにリダイレクトすること it "redirects to the dashboard" do # 「@user」としてログイン sign_in @user # @userのidを渡して、other_userの@taskのshowページにアクセスする get :show, params: { id: @task.id } # ルートページに遷移することを確認 expect(response).to redirect_to root_path end end end
createアクション(POST)のSpec
createアクションで確認すること
- ログイン済みのユーザーであれば、新しいTaskが作成できる
- 未ログインの場合は、ログインページにリダイレクトされる
describe "#create" do # 未ログインのユーザーの場合 context "as a user not to login" do it "returns a 302 response" do # Taskのインスタンスを生成するための情報を「task_params」に格納 task_params = FactoryBot.attributes_for(:task) #「task_params」をPOSTで送信する post :create, params: {task: task_params } # レスポンスのステータスが「302(失敗)」であることを確認 expect(response).to have_http_status "302" end # サインイン画面にリダイレクトすること it "redirects to the sign-in page" do # Taskのインスタンスを生成するための情報を「task_params」に格納 task_params = FactoryBot.attributes_for(:task) #「task_params」をPOSTで送信する post :create, params: {task:task_params } # サインインページに遷移することを確認 expect(response).to redirect_to "/users/sign_in" end end end
updateアクション(PATCH)のSpec
updateアクションで確認すること
- ログイン済みのユーザーでも、他人のプロジェクトは編集できない
- 未ログインのユーザーが実行すると、ログインページにリダイレクトされる
describe "#update" do # 別ユーザーのTaskを更新しようとした場合 context "if logged in user updates another user" do # 各example(it〜end)の前に「@user」「other_user」「(other_userの)@task」を作成 before do @user = FactoryBot.create(:user) other_user = FactoryBot.create(:user) @task = FactoryBot.create(:task, owner: other_user, name: "Same Old Name") end # プロジェクトを更新できないことを確認 it "does not update the task" do # nameを更新するための式を、変数「task_params」に格納 task_params = FactoryBot.attributes_for(:task, name: "New Name") # @userとしてログイン sign_in @user #「task_params」を、@taskに対してPATCHで送信する patch :update, params: { id: @task.id,task: task_params } # @taskのnameを更新しても、変更されていないことを確認 expect(@task.reload.name).to eq "Same Old Name" end # ルートページへリダイレクトすること it "redirects to the dashboard" do # Taskのインスタンスを生成するための情報を「task_params」に格納 task_params = FactoryBot.attributes_for(:task) # @userとしてログイン sign_in @user #「task_params」を、@taskに対してPATCHで送信する patch :update, params: { id: @task.id,task: task_params } # ルートページに遷移することを確認 expect(response).to redirect_to root_path end end # 未ログインのユーザーの場合 context "as a user not to login" do # @taskを生成 before do @task = FactoryBot.create(:task) end # 302レスポンスを返すこと it "returns a 302 response" do # Taskのインスタンスを生成するための情報を「task_params」に格納 task_params = FactoryBot.attributes_for(:task) #「task_params」を、@taskに対してPATCHで送信する patch :update, params: { id: @task.id,task: task_params } # レスポンスのステータスが「302(失敗)」であることを確認 expect(response).to have_http_status "302" end # サインイン画面にリダイレクトすること it "redirects to the sign-in page" do # Taskのインスタンスを生成するための情報を「task_params」に格納 task_params = FactoryBot.attributes_for(:task) #「task_params」を、@taskに対してPATCHで送信する patch :update, params: { id: @task.id,task: task_params } # サインインページに遷移することを確認 expect(response).to redirect_to "/users/sign_in" end end end
destroyアクション(DELETE)のSpec
destroyアクションで確認すること
- ログイン済みのユーザーでも、他人のTaskかは削除できない
- 未ログインのユーザーが削除しようとすると、ログインページにリダイレクトされる
describe "#destroy" do # 別ユーザーのTaskを削除しようとした場合 context "if logged in user deletes another user" do # 各example(it〜end)の前に「@user」「other_user」「(other_userの)@task」を作成 before do @user = FactoryBot.create(:user) other_user = FactoryBot.create(:user) @task = FactoryBot.create(:task, owner: other_user) end # プロジェクトを削除できないこと it "does not delete thetask" do # @userとしてログインする sign_in @user # DELETEを@taskに対して送信し、Taskの数が変わらないことを確認 expect { delete :destroy, params: { id: @task.id } }.to_not change(Task, :count) end # ルートページにリダイレクトすること it "redirects to the dashboard" do # @userとしてログインする sign_in @user # DELETEを@taskにして送信する delete :destroy, params: { id: @task.id } # ルートページに遷移することを確認 expect(response).to redirect_to root_path end end # 未ログインのユーザーの場合 context "as a user not to login" do # 各example(it〜end)の前に「@task」を作成 before do @task = FactoryBot.create(:task) end # 302レスポンスを返すこと it "returns a 302 response" do # DELETEを@taskに対して送信する delete :destroy, params: { id: @task.id } # レスポンスのステータスが「302(失敗)」であることを確認 expect(response).to have_http_status "302" end # サインイン画面にリダイレクトすること it "redirects to the sign-in page" do # DELETEを@taskに対して送信する delete :destroy, params: { id: @task.id } # サインインページに遷移することを確認 expect(response).to redirect_to "/users/sign_in" end # プロジェクトを削除できないこと it "does not delete the task" do # DELETEを@taskに対して送信し、Taskの数が変わらないことを確認 expect { delete :destroy, params: { id: @task.id } }.to_not change(Task, :count) end end end
おまけ(各アクションが成功する場合のテスト)
最初に述べましたが、各アクションが成功する場合のテストはFeatureSpecで行うのが通例です。
ただ、必要な方がいるかもしれないので、念のため載せておきますね。
indexアクションのspec
indexアクションの成功パターン
- ログイン済みの場合は、indexページを表示
# ログイン済みのユーザーの場合 context "as an authenticated user" do # 各example(it〜end)の前に「@user」を作成 before do @user = FactoryBot.create(:user) end # 正常にレスポンスを返すこと it "responds successfully" do # 「@user」としてログイン sign_in @user # indexアクションを行うと、レスポンスがあることを確認 get :index expect(response).to be_successful end # 200レスポンスを返すこと it "returns a 200 response" do # 「@user」としてログイン sign_in @user # indexアクションを行うと、レスポンスのステータスが「200(成功)」であることを確認 get :index expect(response).to have_http_status "200" end end
showアクションのspec
showアクションの成功パターン
- ログイン済みの場合は、showページを表示
# ログイン済みのユーザーの場合 context "as an authorized user" do # 各example(it〜end)の前に「@user」「(@userの)@task」を作成 before do @user = FactoryBot.create(:user) @task = FactoryBot.create(:task, owner: user) end # 正常にレスポンスを返すこと it "responds successfully" do # 「@user」としてログイン sign_in @user # @userのidを渡して、showアクションを行う get :show, params: { id: @user.id } # レスポンスのステータスが「200(成功)」であることを確認 expect(response).to be_successful end end
createアクションのspec
createアクションの成功パターン
- ログイン済みの場合は、新しいプロジェクトが作成できる
# ログイン済みのユーザーの場合 context "as an authenticated user" do # 各example(it〜end)の前に「@user」を作成 before do @user = FactoryBot.create(:user) end # 有効な属性値の場合 context "with valid attributes" do # プロジェクトを追加できること it "adds atask" do # Taskのインスタンスを生成するための情報を「task_params」に格納 task_params = FactoryBot.attributes_for(:task) # @userとしてログイン sign_in @user #「task_params」をtaskに対してPOSTで送信し、@userのTaskの数が1つ増えることを確認 expect { post :create, params: { task: task_params } }.to change(@user.tasks, :count).by(1) end end # 無効な属性値の場合 context "with invalid attributes" do # プロジェクトを追加できないこと it "does not add a task" do # Taskのnameをnilで更新するための情報を「task_params」に格納 task_params = FactoryBot.attributes_for(:task, name: nil) # @userとしてログイン sign_in @user #「task_params」をtaskに対してPOSTで送信し、@userのTaskの数が変わらないことを確認 expect { post :create, params: { task: task_params } }.to_not change(@user.tasks, :count) end end end # プロジェクトを追加できること it "adds a task" do # Taskのインスタンスを生成するための情報を「task_params」に格納 task_params = FactoryBot.attributes_for(:task) # @userとしてログイン sign_in @user #「task_params」をPOSTで送信した際、@userのtaskが1つ増えることを確認 expect { post :create, params: { task: task_params } }.to change(@user.tasks, :count).by(1) end
updateアクションのSpec
updateアクションの成功パターン
- ログイン済みの場合、自分のTaskは更新できる
# ログイン済みのユーザーの場合 context "as an authorized user" do # 各example(it〜end)の前に「@user」「(@userの)@task」を作成 before do @user = FactoryBot.create(:user) @task = FactoryBot.create(:task, owner: @user) end # プロジェクトを更新できることを確認 it "updates a task" do # @taskのnameカラムを"New Task Name"に更新するための引数を「task_params」に格納 task_params = FactoryBot.attributes_for(:task, name: "New Task Name") # 「@user」としてログイン sign_in @user #「task_params」をPATCHで送信し、自分の保有しているtaskのDBの値を更新する patch :update, params: { id: @task.id,task: task_params } # @taskのnameカラムを更新し、"New Task Name"になっていることを確認 expect(@task.reload.name).to eq "New Task Name" end end
destroyアクションのSpec
destroyアクションの成功パターン
- ログイン済みの場合、自分のTaskは削除できる
# ログイン済みのユーザーの場合 context "as an authorized user" do # 各example(it〜end)の前に「@user」「(@userの)@task」を作成 before do @user = FactoryBot.create(:user) @task = FactoryBot.create(:task, owner: @user) end # プロジェクトを削除できること it "deletes a task" do # @userとしてログイン sign_in @user # DELETEを@taskに対して送信し、Taskの数が1つ減ることを確認 expect { delete :destroy, params: { id: @task.id } }.to change(@user.tasks, :count).by(-1) end end