【Rails】RSpecコントローラ(Controller)テストの書き方

スポンサーリンク

 

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

困ったマン
RailsのテストとしてRSpecを使っているけど、コントローラーのテストって何すればいいか分かんない…

こんなお悩みに答えます。

 

スポンサーリンク

コントローラーのSpecは、あんまり重要じゃない

まず1番肝心なことをお伝えすると、コントローラーのSpecは優先度・重要度が低いです。

なぜなら、アクセスを行った際にアプリがどんな動きをするかはFeatureSpecて(ブラウザでの動作をシミュレートするテスト)で検証するのがメインだから。

そのため、コントローラのSpecは、基本的にアクセス制御(不正なユーザー登録をはじくとか)が機能しているかを確認する程度であることが多いです。

例えば、コントローラのテストでユーザーの作成が成功したとしても、「ブラウザにユーザー作成ボタンがなかった」みたいな凡ミスがあるかもしれませんよね?
こうしたミスを防ぐ意味でも、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

 

Twitterフォローしてね!!!

有益な無益をつぶやくよ↓↓↓

にょけん

スポンサーリンク