Help us understand the problem. What is going on with this article?

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #10.5 RSpecでTutorialのテストを書き直す

こんなことが分かる

  • TutorialでのMinitestをRSpecで変換する方法
  • Controller/Model spec, Request spec, System specなどの違い

前回:#10 リメンバーミー機能編
次回:#11 プロフィール編集編

注意点

  • Tutorial9章までまたは本記事#10までのテストをRSpecで見直します
  • Minitest → RSpecに移行する都合上、異なるファイル名を使用する場合があります
  • リファクタリングの都合上、別テストに移行、再定義、example名の変更などを行なっています

リンク先:
Ruby on Rails Tutorial
Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #1 準備編

当記事の背景

何specで書けばいいのかを区別するためです。

テストに関するガイドライン

  • Contoller/View specは極力不使用
  • Request specは結合テストで使用
  • System specはブラウザ上のテストに使用

Request spec → コントローラやヘルパーなどがどう行動したのか
System spec → ブラウザ画面がどうなっているのか

この基準で進めます。

Tutorial9章/記事#10までのRSpec公開

以下の順で公開します。

準備系

  • rails_helper.rb
  • factories
  • support

テスト系

  • models
  • helpers
  • requests
  • systems

rails_helper.rb

意義:RSpecの設定

spec/rails_helper.rb
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'

Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

begin
  ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
  puts e.to_s.strip
  exit 1
end

RSpec.configure do |config|
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = true
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
  config.include FactoryBot::Syntax::Methods
  config.include ApplicationHelpers

  config.before(:each) do |example|
    if example.metadata[:type] == :system
      driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
    end
  end
end

factories

意義:テスト用のActive Record

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Michael Example" }
    email { "michael@example.com" }
    password { "password" }
    password_confirmation { "password" }
  end
end

support

意義:テスト用のヘルパー

spec/support/application_helper.rb
module ApplicationHelpers

  def is_logged_in?
    !session[:user_id].nil?
  end
end

models

意義:モデルのテスト

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do

  let(:user) { create(:user) }

  describe "User" do
    it "should be valid" do
      expect(user).to be_valid
    end
  end

  describe "name" do
    it "gives presence" do
      user.name = "  "
      expect(user).to be_invalid
    end

    context "50 characters" do
      it "is not too long" do
        user.name = "a" * 50
        expect(user).to be_valid
      end
    end

    context "51 characters" do
      it "is too long" do
        user.name = "a" * 51
        expect(user).to be_invalid
      end
    end
  end

  describe "email" do
    it "gives presence" do
      user.email = "  "
      expect(user).to be_invalid
    end

    context "254 characters" do
      it "is not too long" do
        user.email = "a" * 243 + "@example.com"
        expect(user).to be_valid
      end
    end

    context "255 characters" do
      it "is too long" do
        user.email = "a" * 244 + "@example.com"
        expect(user).to be_invalid
      end
    end

    it "should accept valid addresses" do
      user.email = "user@example.com"
      expect(user).to be_valid
      user.email = "USER@foo.COM"
      expect(user).to be_valid
      user.email = "A_US-ER@foo.bar.org"
      expect(user).to be_valid
      user.email = "first.last@foo.jp"
      expect(user).to be_valid
      user.email = "alice+bob@baz.cn"
      expect(user).to be_valid
    end

    it "should reject invalid addresses" do
      user.email = "user@example,com"
      expect(user).to be_invalid
      user.email = "user_at_foo.org"
      expect(user).to be_invalid
      user.email = "user.name@example."
      expect(user).to be_invalid
      user.email = "foo@bar_baz.com"
      expect(user).to be_invalid
      user.email = "foo@bar+baz.com"
      expect(user).to be_invalid
      user.email = "foo@bar..com"
      expect(user).to be_invalid
    end

    it "should be unique" do
      duplicate_user = user.dup
      duplicate_user.email = user.email.upcase
      user.save!
      expect(duplicate_user).to be_invalid
    end

    it "should be saved as lower-case" do
      user.email = "Foo@ExAMPle.CoM"
      user.save!
      expect(user.reload.email).to eq 'foo@example.com'
    end
  end

  describe "password and password_confirmation" do
    it "should be present (nonblank)" do
      user.password = user.password_confirmation = " " * 6
      expect(user).to be_invalid
    end

    context "5 characters" do
      it "is too short" do
        user.password = user.password_confirmation = "a" * 5
        expect(user).to be_invalid
      end
    end

    context "6 characters" do
      it "is not too short" do
        user.password = user.password_confirmation = "a" * 6
        expect(user).to be_valid
      end
    end
  end

  describe "User model methods" do
    describe "authenticated?" do
      it "return false for a user with nil digest" do
        expect(user.authenticated?('')).to be_falsey
      end
    end
  end
end

helpers

意義:ヘルパーのテスト

spec/helpers/application_helper_spec.rb
require 'rails_helper'

RSpec.describe ApplicationHelper, type: :helper do

  describe "#full_title" do
    context "page_title is empty" do
      it "removes symbol" do
        expect(helper.full_title).to eq('Lantern Lantern')
      end
    end

    context "page_title is not empty" do
      it "returns title and application name where contains symbol" do
        expect(helper.full_title('hoge')).to eq('hoge | Lantern Lantern')
      end
    end
  end
end
spec/helpers/sessions_helper_spec.rb
require 'rails_helper'

RSpec.describe SessionsHelper, type: :helper do

  let(:user) { create(:user) }

  describe "#current_user" do
    it "returns right user when session is nil" do
      remember(user)
      expect(current_user).to eq user
      expect(is_logged_in?).to be_truthy
    end

    it "returns nil when remember digest is wrong" do
      remember(user)
      user.update_attribute(:remember_digest, User.digest(User.new_token))
      expect(current_user).to be_nil
    end
  end
end

requests

意義:結合テスト

spec/requests/users_logins_spec.rb
require 'rails_helper'

RSpec.describe "UsersLogins", type: :request do
  include SessionsHelper

  let(:user) { create(:user) }

  def post_invalid_information
    post login_path, params: {
      session: {
        email: "",
        password: ""
      }
    }
  end

  def post_valid_information(remember_me = 0)
    post login_path, params: {
      session: {
        email: user.email,
        password: user.password,
        remember_me: remember_me
      }
    }
  end

  describe "GET /login" do
    context "invalid form information" do
      it "fails having a danger flash message" do
        get login_path
        post_invalid_information
        expect(flash[:danger]).to be_truthy
        expect(is_logged_in?).to be_falsey
      end
    end

    context "valid form information" do
      it "succeeds having no danger flash message" do
        get login_path
        post_valid_information
        expect(flash[:danger]).to be_falsey
        expect(is_logged_in?).to be_truthy
        follow_redirect!
        expect(request.fullpath).to eq '/users/1'
      end

      it "succeeds logout" do
        get login_path
        post_valid_information
        expect(is_logged_in?).to be_truthy
        follow_redirect!
        expect(request.fullpath).to eq '/users/1'
        delete logout_path
        expect(is_logged_in?).to be_falsey
        follow_redirect!
        expect(request.fullpath).to eq '/'
      end

      it "does not log out twice" do
        get login_path
        post_valid_information
        expect(is_logged_in?).to be_truthy
        follow_redirect!
        expect(request.fullpath).to eq '/users/1'
        delete logout_path
        expect(is_logged_in?).to be_falsey
        follow_redirect!
        expect(request.fullpath).to eq '/'
        delete logout_path
        follow_redirect!
        expect(request.fullpath).to eq '/'
      end

      it "succeeds remember_token because of check remember_me" do
        get login_path
        post_valid_information(1)
        expect(is_logged_in?).to be_truthy
        expect(cookies[:remember_token]).not_to be_empty
      end

      it "has no remember_token because of check remember_me" do
        get login_path
        post_valid_information(0)
        expect(is_logged_in?).to be_truthy
        expect(cookies[:remember_token]).to be_nil
      end

      it "has no remember_token when users logged out and logged in" do
        get login_path
        post_valid_information(1)
        expect(is_logged_in?).to be_truthy
        expect(cookies[:remember_token]).not_to be_empty
        delete logout_path
        expect(is_logged_in?).to be_falsey
        expect(cookies[:remember_token]).to be_empty
      end
    end
  end
end
spec/requests/users_signups_spec.rb
require 'rails_helper'

RSpec.describe "UsersSignups", type: :request do
  include SessionsHelper

  def post_invalid_information
    post signup_path, params: {
      user: {
        name: "",
        email: "user@invalid",
        password: "foo",
        password_confirmation: "bar"
      }
    }
  end

  def post_valid_information
    post signup_path, params: {
      user: {
        name: "Example User",
        email: "user@example.com",
        password: "password",
        password_confirmation: "password"
      }
    }
  end

  describe "GET /signup" do
    it "is invalid signup information" do
      get signup_path
      expect { post_invalid_information }.not_to change(User, :count)
      expect(is_logged_in?).to be_falsey
    end

    it "is valid signup information" do
      get signup_path
      expect { post_valid_information }.to change(User, :count).by(1)
      expect(is_logged_in?).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
    end
  end
end

systems

意義:ブラウザテスト

spec/systems/login_spec.rb
require 'rails_helper'

RSpec.describe "Logins", type: :system do

  let(:user) { create(:user) }

  def submit_with_invalid_information
    fill_in 'メールアドレス', with: ''
    fill_in 'パスワード', with: ''
    find(".form-submit").click
  end

  def submit_with_valid_information(remember_me = 0)
    fill_in 'メールアドレス', with: user.email
    fill_in 'パスワード', with: 'password'
    check 'session_remember_me' if remember_me == 1
    find(".form-submit").click
  end

  describe "Login" do
    context "invalid" do
      it "has no information and has flash danger message" do
        visit login_path
        expect(page).to have_selector '.login-container'
        submit_with_invalid_information
        expect(current_path).to eq login_path
        expect(page).to have_selector '.login-container'
        expect(page).to have_selector '.alert-danger'
      end

      it "deletes flash messages when users input invalid information then other links" do
        visit login_path
        submit_with_invalid_information
        expect(current_path).to eq login_path
        visit root_path
        expect(page).not_to have_selector '.alert-danger'
      end
    end

    context "valid" do
      it "has valid information and will link to user path" do
        visit login_path
        submit_with_valid_information
        expect(current_path).to eq user_path(1)
        expect(page).to have_selector '.show-container'
      end

      it "contains logout button without login button at user path" do
        visit login_path
        submit_with_valid_information
        expect(current_path).to eq user_path(1)
        expect(page).to have_selector '.btn-logout-extend'
        expect(page).not_to have_selector '.btn-login-extend'
      end
    end
  end

  describe "Logout" do 
    it "contains login button without logout button at root path" do
      visit login_path
      submit_with_valid_information
      expect(current_path).to eq user_path(1)
      expect(page).to have_selector '.btn-logout-extend'
      expect(page).not_to have_selector '.btn-login-extend'
      click_on 'ログアウト'
      expect(current_path).to eq root_path
      expect(page).to have_selector '.home-container'
      expect(page).to have_selector '.btn-login-extend'
      expect(page).not_to have_selector '.btn-logout-extend'
    end
  end
end
spec/systems/signup_spec.rb
require 'rails_helper'

RSpec.describe "Signups", type: :system do

  def submit_with_invalid_information
    fill_in '名前', with: ''
    fill_in 'メールアドレス', with: 'user@invalid'
    fill_in 'パスワード', with: 'foo'
    fill_in 'パスワード(再入力)', with: 'bar'
    find(".form-submit").click
  end

  def submit_with_valid_information
    fill_in '名前', with: 'Example User'
    fill_in 'メールアドレス', with: 'user@example.com'
    fill_in 'パスワード', with: 'password'
    fill_in 'パスワード(再入力)', with: 'password'
    find(".form-submit").click
  end

  it "is invalid because it has no name" do
    visit signup_path
    submit_with_invalid_information
    expect(current_path).to eq signup_path
    expect(page).to have_selector '#error_explanation'
  end

  it "is valid because it fulfils form information" do
    visit signup_path
    expect { submit_with_valid_information }.to change(User, :count).by(1)
    expect(current_path).to eq user_path(1)
    expect(page).not_to have_selector '#error_explanation'
  end
end
spec/systems/site_layout_spec.rb
require 'rails_helper'

RSpec.describe "SiteLayouts", type: :system do

  describe "home layout" do
    it "contains root link" do
      visit root_path
      expect(page).to have_link nil, href: root_path
    end

    it "contains signup link" do
      visit root_path
      expect(page).to have_link 'はじめる', href: signup_path
    end

    it "contains login link" do
      visit root_path
      expect(page).to have_link 'ログイン', href: login_path
    end

    it "returns title with 'Lantern Lantern'" do
      visit root_path
      expect(page).to have_title 'Lantern Lantern'
    end
  end

  describe "about layout" do
    it "returns title with 'About | Lantern Lantern'" do
      visit about_path
      expect(page).to have_title 'About | Lantern Lantern'
    end
  end
end

前回:#10 リメンバーミー機能編
次回:#11 プロフィール編集編

aokyo17
rails tutorial → ポートフォリオing. 誰もが経験した初心びくびく20代1年目.. フォローはすぐ返したい厨。
https://komucha.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away