Edited at

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で書けばいいの?」といったなんとなくを区別するため。

#10までの間、Tutorialを元にした結合テストをSystem specで書いていた。

しかしSystem specはあくまでブラウザを操作してのテスト。

コントローラやヘルパーなどが絡む内部的な確認はRequest specで行うべき。

加えてSystem specでRequest specを補おうと思えば限界がある。

しっかり区別した上で書かないと支障をきたす。

よって当記事で扱うポートフォリオ加えTutorialのテストをRSpecで見直しました。


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


  • Contoller/View specは使用しない

  • Model/Helper 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 プロフィール編集編