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

# こんなことが分かる
* TutorialでのMinitestをRSpecで変換する方法
* Controller/Model spec, Request spec, System specなどの違い

前回：[#10 リメンバーミー機能編](https://qiita.com/aokyo17/items/786f6cb316a2323c0bf2)
次回：[#11 プロフィール編集編](https://qiita.com/aokyo17/items/0a7d7dcfc28e705f5610)

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

リンク先：
[Ruby on Rails Tutorial](https://railstutorial.jp/chapters/beginning?version=5.1#cha-beginning)
[Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #1 準備編](https://qiita.com/aokyo17/items/4df4cfd97891addc0452)


# 当記事の背景
何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の設定

```rb: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

```rb: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
意義：テスト用のヘルパー

```rb:spec/support/application_helper.rb
module ApplicationHelpers
  
  def is_logged_in?
    !session[:user_id].nil?
  end
end
```

## models
意義：モデルのテスト

```rb: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
```

```rb: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
意義：結合テスト

```rb: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
```

```rb: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
意義：ブラウザテスト

```rb: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
```

```rb: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
```

```rb: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 リメンバーミー機能編](https://qiita.com/aokyo17/items/786f6cb316a2323c0bf2)
次回：[#11 プロフィール編集編](https://qiita.com/aokyo17/items/0a7d7dcfc28e705f5610)