Ruby
Rails
RSpec
FactoryGirl
テスト

Railsチュートリアル テストをRSpecで実施 【フィーチャスペック編 authorization_spec】

テスト対象

該当するコントローラ 該当するアクション
UsersController index, create, edit, update, destroy, following, followers
MicropostsController create, destroy

環境

Userモデル 単体テスト編 1/3 に記載

フォルダ、使用ファイル

種類 ファイル名
スペック spec/features/authorization_spec.rb
サポートモジュール spec/support/support_module.rb
shared_context spec/support/shared_context.rb
shared_examples spec/support/shared_examples.rb
ファクトリ(ユーザ) spec/support/factories/users.rb
ファクトリ(マイクロポスト) spec/support/factories/microposts.rb

未ログインの場合(ページ保護)のスペック

アウトラインの作成

authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do
  # アクセス権限
  describe "in UsersController"
    # 未ログインの場合 (before_action のテスト)
    describe "login is necessary"
      context "when non-login"
        describe "index"
          # エラーメッセージ、ログインページにリダイレクト
        describe "edit"
          # エラーメッセージ、ログインページにリダイレクト
        describe "update"
          # エラーメッセージ、ログインページにリダイレクト
        describe "destroy"
          # エラーメッセージ、ログインページにリダイレクト
        describe "following"
          # エラーメッセージ、ログインページにリダイレクト
        describe "followers"
          # エラーメッセージ、ログインページにリダイレクト
    # ユーザ削除権限
    describe "authorization of delete user"
      # adminユーザの場合
      context "admin"
        # ユーザの削除ができること
        # 成功メッセージ
      # 一般ユーザの場合
      context "non-admin"
        # ユーザの削除ができないこと
        # ルートにリダイレクトされること

  describe "in MicropostsController"
    # マイクロポスト 投稿(作成)/削除
    describe "authorization of create/destroy micropost"
      context "when non-login"
        describe "create"
          # エラーメッセージ、ログインページにリダイレクト
        describe "destroy"
          # エラーメッセージ、ログインページにリダイレクト
end

shared_examples の作成 1/2

  • ログインメッセージ出力・ログインページへリダイレクトは、一般化して使い回せるようにしてみる

  • spec側でブロックをオブジェクト化(Proc.new { })して subject { } に定義し、shared_examples側で、subject.call して呼び出す

shared_examples.rb
# spec/support/shared_examples.rb

  # 成功メッセージ
  # flash[:success]
  shared_examples_for "success message" do |msg|
    it { subject.call; expect(flash[:success]).to eq msg }
  end

  # 失敗メッセージ
  # flash[:danger]
  shared_examples_for "error message" do |msg|
    it { subject.call; expect(flash[:danger]).to eq msg }
  end

  # リダイレクト
  # redirect to path
  shared_examples_for "redirect to path" do |path|
    it { subject.call; expect(response).to redirect_to path }
  end

shared_examples の作成 2/2

↓のように、ユーザの削除(admin権限)成功/失敗のスペックを作成

  • it_behaves_like "success delete user"
  • it_behaves_like "fail delete user"
shared_examples.rb
# spec/support/shared_examples.rb

  # ユーザの削除(admin権限)
  # users#destroy

  # 成功
  shared_examples_for "success delete user" do
    scenario "user decrememt -1" do
      login_as(admin)
      click_link "Users"
      expect(page).to have_current_path("/users")
      expect(page).to have_link('delete', href: user_path(User.first))
      expect(page).to have_link('delete', href: user_path(User.second))
      expect(page).not_to have_link('delete', href: user_path(admin))
      expect {
        click_link('delete', match: :first)
        # 成功メッセージ
        expect(page).to have_css("div.alert.alert-success", text: "User deleted")
      }.to change(User, :count).by(-1)
    end
  end

  # 失敗
  shared_examples_for "fail delete user" do
    scenario "user decrement 0" do
      login_as(user)
      click_link "Users"
      expect(page).to have_current_path("/users")
      expect(page).not_to have_link('delete', href: user_path(User.first))
      expect(page).not_to have_link('delete', href: user_path(User.second))
      expect {
        # リンクが無いので、直接 HTTPリクエストを発行
        delete user_path(User.first)
      }.to change(User, :count).by(0)
    end
  end

スペック作成

authorization_spec.rb
# spec/features/authorization_spec.rb
require 'rails_helper'

RSpec.feature "Authorization", type: :feature do

  include SupportModule
  include_context "setup"

  # アクセス権限
  describe "in UsersController", type: :request do
    describe "login is necessary" do
      context "when non-login" do
        describe "index" do
          subject { Proc.new { get users_path } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "edit" do
          subject { Proc.new { get edit_user_path(user) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "update" do
          subject { Proc.new { patch user_path(user), params: { user: update_params_1 } } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "destroy" do
          subject { Proc.new { delete user_path(other_user) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "following" do
          subject { Proc.new { get following_user_path(user) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "followers" do
          subject { Proc.new { get followers_user_path(user) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
      end
    end
    # ユーザ削除権限
    describe "authorization of delete user" do
      before { users }
      it { expect(User.count).to eq users.count }
      # adminユーザの場合
      context "admin" do
        it_behaves_like "success delete user"
      end
      # 一般ユーザの場合
      context "non-admin" do
        it_behaves_like "fail delete user"
        it { expect(page).to have_current_path("/") }
      end
    end
  end

  describe "in MicropostsController", type: :request do
    # マイクロポスト 投稿(作成)/削除
    describe "authorization of create/destroy micropost" do
      context "when non-login" do
        describe "create" do
          subject { Proc.new { post microposts_path, params: post_params } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
        describe "destroy" do
          subject { Proc.new { delete micropost_path(my_post.id) } }
          it_behaves_like "error flash", "Please log in"
          it_behaves_like "redirect to path", "/login"
        end
      end
    end
  end
end

SupportModule, shared_context, ファクトリ

  • SupportModule
support_module.rb
  # spec/support/support_module.rb
  module SupportModule
    def login_as(user)
      visit root_path
      click_link "Log in"
      fill_in "Email",    with: user.email
      fill_in "Password", with: user.password
      click_button "Log in"
    end
  end
  • shared_context
shared_context.rb
  # spec/support/shared_context.rb
  RSpec.shared_context "setup" do
    # ユーザ
    # 遅延評価、呼ばれた時にDB保存される
    let(:user) { create(:user) }
    let(:admin) { create(:admin) }

    # マイクロポスト
    # 自分の投稿
    let(:my_post) { create(:user_post) }
    # 属性をハッシュ化して呼ばれた時に使う
    let(:post_params) { attributes_for(:user_post) }
  end
  • ファクトリ
users.rb
  # spec/factories/users.rb
  FactoryBot.define do
    # 自分
    # factory [任意のファクトリ名], class: [クラス名]
    factory :user, class: User do
      name     "Example user"
      email    "user@example.com"
      password              "foobar"
      password_confirmation "foobar"
      admin false
      # 他人
      factory :other_user do # => 入れ子にしているので[クラス名]は不要
        name { Faker::Name.name }
        email { Faker::Internet.email }
        # 管理者ユーザ
        factory :admin do
          admin true
        end
      end
    end
  end
microposts.rb
  # spec/factories/microposts.rb
  FactoryBot.define do
    # 自分のマイクロポスト
    factory :user_post, class: Micropost do
      content { Faker::Lorem.sentence(5) }
      association :user, factory: :user
      # 他人のマイクロポスト
      factory :other_user_post do
        content { Faker::Lorem.sentence(5) }
        association :user, factory: :other_user
      end
    end
  end

実行結果

$ bin/rspec spec/features/authorization_spec.rb
Running via Spring preloader in process 17859

Authorization
  in UsersController
    login is necessary
      when non-login
        index
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        edit
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        update
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        destroy
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        following
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        followers
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
    authorization of delete user
      should eq 30
      admin
        behaves like success delete user
          user decrememt -1
      non-admin
        should have current path "/"
        behaves like fail delete user
          user decrement 0
  in MicropostsController
    authorization of create/destroy micropost
      when non-login
        create
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"
        destroy
          behaves like error flash
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"

Finished in 13.41 seconds (files took 2.92 seconds to load)
20 examples, 0 failures


参考


以上