Ruby
Rails
RSpec
FactoryGirl
テスト

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

テスト対象

該当するコントローラ 該当するアクション
UsersController index

環境

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

フォルダ、使用ファイル

種類 ファイル名
スペック spec/features/users_index_spec.rb
スペック 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

アウトラインの作成 1/3

users_index_spec.rb
# spec/features/users_index_spec.rb
RSpec.feature "UsersIndex", type: :feature do

  # 未ログイン場合のスペックは、
  # authorization_spec.rb にて(別ファイル)作成

  describe "index"
    # ページネーション
    describe "pagination"
      # ページネーションでユーザが表示されること
      scenario "list each user"

end

スペック作成 1/3

users_index_spec.rb
# spec/features/users_index_spec.rb

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

  include SupportModule
  include_context "setup"

  subject { page }

  describe "index" do
    # ページネーション
    describe "pagination" do
      # (ファクトリをセット)
      before { users }  # => shared_context 内で定義
      # (セットアップの確認)
      it { expect(User.count).to eq users.count }
      # ページネーションでユーザが表示されること
      scenario "list each user" do
        login_as(user)  # => SupportModule 内で定義
        click_link "Users"
        should have_current_path("/users")
        should have_title("All users")
        should have_css("h1", text: "All users")
        User.paginate(page: 1).each do |user|
          expect(page).to have_css("li", text: user.name)
        end
      end
    end
  end
end

shared_context "setup"の作成、ファクトリの作成

  • before { users } の部分
shared_context.rb
  # spec/support/shared_context.rb
  RSpec.shared_context "setup" do

    # 遅延評価、呼ばれた時にDB保存される
    let(:users) { create_list(:other_user, 30) }

  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 }
      end
    end
  end

SupportModule 作成

  • login_as(user) の部分
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

実行結果 1/3

$ bin/rspec spec/features/users_index_spec.rb

UsersIndex
  index
    pagination
      should eq 30
      list each user

Finished in 5.22 seconds (files took 2.1 seconds to load)
2 examples, 0 failures


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

  • authorization_spec.rb (別ファイル)にて作成

アウトラインの作成 2/3

authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do

  # ページ保護(アクセス権限)
  describe "in UsersController"
    describe "login is necessary"
      # 未ログインの場合
      context "when non-login"
        describe "index"
          # ログインメッセージが出力されること
          it "should have error_messages 'Please log in'"
          # ログインページへリダイレクトされること
          it "should have redirect to '/login'"
end

shared_examples の作成

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

  • 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

スペック作成 2/3

  • spec側でブロックをオブジェクト化(Proc.new { })して subject { } に定義
  • get users_path の部分は、直接HTTPリクエストを送るので、オプション type: :request をつけないと NoMethodError になってしまう
authorization_spec.rb
# spec/features/authorization_spec.rb
require 'rails_helper'

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

  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
      end
    end
  end
end

実行結果 2/3

$ bin/rspec spec/features/authorization_spec.rb -e "index"

Authorization
  in UsersController
    login is necessary
      when non-login
        index
          behaves like error message
            should eq "Please log in"
          behaves like redirect to path
            should redirect to "/login"

Finished in 1.64 seconds (files took 2.31 seconds to load)
2 examples, 0 failures

ユーザ削除権限(admin権限)のスペック

  • authorization_spec.rb (別ファイル)にて作成

アウトライン作成 3/3

authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do

  describe "in UsersController", type: :request do

    # (省略)

    # ユーザ削除権限
    describe "authorization of delete user"
      # adminユーザの場合
      context "admin"
        # ユーザの削除ができること
        it "success create user"
        # 成功メッセージ
        it "have success messages"
      # 一般ユーザの場合
      context "non-admin"
        # ユーザの削除ができないこと
        it "fail delete user"
        # ルートにリダイレクトしていること
        it "have current path '/'"
end

スペック作成 3/3

authorization_spec.rb
# spec/features/authorization_spec.rb
RSpec.feature "Authorization", type: :feature do

  include SupportModule     # => login_as(admin) を使う
  include_context "setup"

  describe "in UsersController", type: :request do

    # (省略)

    # ユーザ削除権限
    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 { should have_current_path("/") }
      end
    end
  end
end

shared_examples の作成

  • 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

実行結果 3/3

$ bin/rspec spec/features/authorization_spec.rb -e "authorization of delete user"

Authorization
  in UsersController
    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

Finished in 8.17 seconds (files took 2.23 seconds to load)
4 examples, 0 failures


参考


続く

フィーチャスペック編 users_signup_spec 2/7