fv17の日記 - Coding Every Day

Webエンジニアの備忘用ブログです。主にWeb界隈の技術に関して書いています。

【各章まとめ】Everyday Rails - RSpecによるRailsテスト入門 - 第5章 コントローラスペック

自分の備忘用に記載。
そのため書籍から内容の増減があります。

本章の前提

  • コントローラのテストは削除、あるいは統合テスト等への移行が推奨
  • そのため、既存システムでコントローラのテストがある場合に備えて学ぶ
  • 筆者は、コントローラのテストはアクセス制御が正しく機能しているか確認するテストに限定

本章で学ぶこと

  • コントローラとモデルのテストの違い
  • コントローラのテスト実装
  • 認証が必要なアクションのテスト実装
  • ユーザーの入力をテスト
  • CSVJSONなどの非HTMLを出力する場合のテスト

コントローラスペックの基本

スペック作成

spec/controllers/home_controller_spec.rbが作成される

bin/rails g rspec:controller home

正しいレスポンスが戻ってくることをテスト

describe "#index" do
  it "responds successfully" do
    get :index
    expect(response).to be_success
  end

  it "responds a 200 response" do
    get :index
    expect(response).to have_http_status "200"
  end
end

ログイン状態をシュミレートしてテストする(Device Gemを用いる場合)

Deviceのテストヘルパーを利用する

spec/rails_helper.rbに追加

config.include Devise::Test::ControllerHelpers, type: :controller

ログインしてからテスト実行

sign_inメソッドでログインできる
spec/controllers/projects_controller_spec.rb

describe "#index" do
  let(:user) { FactoryBot.create(:user) }

  it "responds successfully" do
    sign_in user
    get :index
    expect(response).to be_success
  end

  it "returns a 200 response" do
    sign_in user
    get :index
    expect(response).to have_http_status "200"
  end
end

ログインせずにアクションを呼び出して失敗することもテスト

  • 302を返す
  • ログインページにリダイレクトすること

を確認
spec/controllers/projects_controller_spec.rb

describe "#index" do
  context "as an authenticated user" do
    (省略)
  end

  context "as a guest" do
    it "responds successfully" do
      get :index
      expect(response).to have_http_status "302"
    end

    it "redirect to the sign-in page" do
      get :index
      expect(response).to redirect_to "/users/sign_in"
    end
  end
end

コントローラのアクションにparams / パラメータを渡す

ユーザーが自分のプロジェクトを表示できることを確認するテストと、
ユーザーが他ユーザーのプロジェクトを表示できないことを確認するテスト

get :show, params: { id: @project.id }の箇所でparamsを渡している

describe "#show" do
  context "as an authenticated user" do
    let(:user) { FactoryBot.create(:user) }
    let(:project) { FactoryBot.create(:project, owner: user) }

    it "responds successfully" do
      sign_in user
      get :show
      expect(response).to be_success
    end

    it "returns a 200 response" do
      sign_in user
      get :show
      expect(response).to have_http_status "200"
    end
  end

  context "as an unauthenticated user" do
    let(:user) { FactoryBot.create(:user) }
    let(:other_user) { FactoryBot.create(:user) }
    let(:project) { FactoryBot.create(:project, owner: other_user) }

    it "responds successfully" do
      sign_in user
      get :show, params: { id: project.id }
      expect(response).to have_http_status "302"
    end

    it "redirect to the sign-in page" do
      sign_in user
      get :show, params: { id: project.id }
      expect(response).to redirect_to root_path
    end
  end
end

ユーザー入力をテストする

createアクションのテスト

  • ログイン済みのユーザーであれば新しいプロジェクトを作成できる
  • ゲストであればアクシ ンへのアクセスを拒否される
describe "#create" do
  context "as an authenticated user" do
    before do
      @user = FactoryBot.create(:user)
    end

    it "adds a project" do
      project_params = FactoryBot.attributes_for(:project)
      sign_in @user
      expect do
        post :create, params: { project: project_params }
      end.to change(@user.projects, :count).by(1)
    end
  end

  # 上記は正常系テストなので、ここに異常系テストを入れるべき。書籍参照

  context "as a guest" do
    it "returns a 302 response" do
      project_params = FactoryBot.attributes_for(:project)
      post :create, params: { project: project_params }
      expect(response).to have_http_status "302"
    end

    it "redirects to the sign-in page" do
      project_params = FactoryBot.attributes_for(:project)
      post :create, params: { project: project_params }
      expect(response).to redirect_to "/users/sign_in"
    end

    # 以下、書籍にはないが必要では?
    it "cannot add a project" do
      expect {
        post :create, params: { project: project_params }
      }.not_to change(Project, :count)
    end
  end
end

updateアクションのテスト

update後、ecpectの記述においてreloadメソッドを呼んでいる点に注意。
こう記述しないと更新後の値が取得できない。

describe"#update"do
  # ログインしており、オーナーである
  context "as an authorized user" do
    let!(:user) { FactoryBot.create(:user) }
    let!(:project) { FactoryBot.create(:project, owner: user) }

    # プロジェクトを更新できる
    it "updates a project" do
      project_params = FactoryBot.attributes_for(:project, name: "New name")
      sign_in user
      patch :update, params: { id: project.id, project: project_params }
      expect(project.reload.name).to eq "New name"
    end
  end

  # ログインしているが、オーナーではない
  context "as an unauthorized user" do
    let!(:user) { FactoryBot.create(:user) }
    let!(:other_user) { FactoryBot.create(:user) }
    let!(:project) { FactoryBot.create(:project,
      owner: other_user,
      name: "Old name"
    ) }

    # プロジェクトを更新できない
    it "does not update the project" do
      project_params = FactoryBot.attributes_for(:project, name: "New name")
      sign_in user
      patch :update, params: { id: project.id, project: project_params }
      expect(project.reload.name).to eq "Old name"
    end

    # ダッシュボードにリダイレクトする
    it "redirects to the dashboard" do
      project_params = FactoryBot.attributes_for(:project, name: "New name")
      sign_in user
      patch :update, params: { id: project.id, project: project_params }
      expect(response).to redirect_to root_path
    end
  end

  # 以下、略

  # ログインしていない
  context "as a guest" do
     # 302レスポンスを返す
     it "returns a 302 response"
     # サインイン画面へリダイレクト
     it "redirects to the sign-in page"
  end
end

destroyアクションのテスト

createとほぼ同じ。
削除できないは、to_notを使う。

expect {
  delete :destroy, params: { id: @project.id }
}.to_not change(Project, :count)

HTML 以外の出力を扱う

省略