こんな人におすすめ
- プログラミング初心者でポートフォリオの作り方が分からない
- Rails Tutorialをやってみたが理解することが難しい
前回:#7 ログイン機能の下準備編
次回:#9 永続セッション, cookie編
今回の流れ
- 今回のゴールを把握する
- ログイン/ログアウトに必要なメソッドを作る
- Sessionsコントローラーを作る
- ヘッダーのボタンを動的にする
- テストを作る
※ この記事は、ポートフォリオを作る理由をweb系自社開発企業に転職するためとします。
※ 2020年4月3日、記事を大幅に更新しました。
今回のゴールを把握する
ゴールは、以下の3つです。
- ログイン/ログアウト機能を実装する
- ユーザー登録時に、ログイン済みにする
- ログイン中は'Logout'、ログアウト中は'Login'ボタンを表示する
まずは、ログイン/ログアウトに必要なメソッドをSessionsヘルパーに4つ作ります。
続いて、ログイン/ログアウトを関係する、Sessionsコントローラーを作ります。
その際、ユーザー登録時にログイン済みにすることも組み込みます。
続いて、ヘッダーにあるログイン/ログアウト時のボタン表示を切り替えます。
その際、Bootstrapでレスポンシブにデザインします。
最後に、ログイン/ログアウト機能のテストを作ります。
その際、テスト用データの作成にFactroyBotを使います。
以上です。
(ログイン画面のビューは#7で完成しているため、省略します。)
ログイン/ログアウトに必要なメソッドを作る
ログイン/ログアウトに必要なメソッドをSessionsヘルパーに作ります。
作るメソッドは以下の4つです。
- ログインを表現する、log_inメソッド
- 現在のユーザーを特定する、current_userメソッド
- ログインしているかどうかを確認する、logged_in?メソッド
- ログアウトを表現する、log_outメソッド
1つ目:log_inメソッドを作る
ログインを表現する、log_inメソッドを作ります。
ログインはsessionメソッドを使うことで表現できます。
引数に与えられるUserモデルのidを、sessionに代入するだけです。
module SessionsHelper
def log_in(user)
session[:user_id] = user.id
end
end
sessionメソッドについて、混同しそうな箇所を解説します。
このメソッドは、以前作成したSessionsコントローラやビューと異なります。
以下のような違いがあります。
- sessionメソッド → Railsに用意された、ログインに便利なメソッド
- Sessionコントローラ/ビュー → ログイン関係のコントローラ/ビューを表現するために、勝手に命名したもの
参考になりました↓
【Rails入門】sessionの使い方まとめ
2つ目:current_userメソッドを作る
現在のユーザーを特定する、current_userメソッドを作ります。
これにより、今後別ページでもユーザーを取り出すことができます。
# 中略
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
# 中略
以下の2つについて解説します。
- findとfind_byの違いについて
- ||= について
findとfind_byの違いについて知る
find_byは#7でも使いましたが、解説をしていませんでした。
よって、ここで取り上げます。
両者には、以下のような違いがあります。
find
- 見つからないとエラーになる
- id以外で探せない
find_by
- 見つからないとnilになる
- id以外でも探せる
current_userメソッドにおいて大事なのは、エラーになるかどうかです。
ログアウトは、sessionを削除することで表現します。
その際、sessionの中身が存在しなくなります。
つまり、ログアウト状態でfindメソッドを使ってしまうとエラーが発生します。
よって、find_byメソッドを使います。
参考になりました↓
find、find_by、whereの違い
find(42)とfind_by(id: 42)の違いについて
||= について知る
||= について解説します。
以下の2つのコードは等価です。
@current_user ||= User.find_by(id: session[:user_id])
@current_user = @current_user || User.find_by(id: session[:user_id])
なじみ深い代入演算子と比べると、理解しやすいかと思います。
x += 1 → x = x + 1
A ||= 1 → A = A || 1
続いて、式の判定について解説します。
判定について、以下の特徴があげられます。
- ||は、trueの場合、処理が終了する
- ||は、左から順に判定する
@current_user || User.find_by(id: session[:user_id])
よって、上記の判定文はこのような意味になります。
current_userが存在する → そのままcurrent_userを代入する
current_userが存在しない → sessionによって探したUserモデルをcurrent_userに代入する
参考になりました↓
【Rails】@current_user ||= User.find_by(id: session[:user_id])という書き方について
3つ目:logged_in?メソッドを作る
ログインしているかどうかを確認する、logged_in?メソッドを作ります。
具体的には、true/falseで確認できるようにします。
これにより、ログイン/ログアウト時のレイアウトが変更できます。
# 中略
def logged_in?
!current_user.nil?
end
end
current_userの前に「!」がついています。
「!」をつけることで、真偽値を反転することができます。
つけないと、current_userがいない時(=ログアウト時)にtrueとなります。
人間の持つ感覚として不適なので、反転させます。
4つ目:log_outメソッドを作る
ログアウトを表現する、log_outメソッドを作ります。
ログアウトは、sessionを削除することで表現します。
# 中略
def log_out
session.delete(:user_id)
@current_user = nil
end
以上で、ログイン/ログアウトに必要なメソッドを4つ完成しました。
Sessionsコントローラーを作る
ログイン/ログアウトを関係する、Sessionsコントローラーを作ります。
#7で作ったコントローラーの拡張になります。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'メールアドレスかパスワードが正しくありません'
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
ユーザー登録時にログイン済みにすることも組み込みます。
class UsersController < ApplicationController
# 中略
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "アカウントの作成が成功しました"
redirect_to @user
else
render 'new'
end
end
# 中略
end
ヘッダーのボタンを動的にする
ヘッダーにあるログイン/ログアウト時のボタン表示を切り替えます。
動的なボタンは、ERb内に簡単なif文を設けることで実現します。
Bootstrapでレスポンシブにデザインします。
<header>
<div class="container">
<nav class="navbar navbar-expand-md navbar-dark fixed-top navbar-extend">
<a class="navbar-brand" herf="#">
<div class="navbar-brand-extend">
<%= link_to image_tag('lantern_lantern_logo.png', width: 50), root_path, class: "logo-img" %>
<%= link_to image_tag('lantern_lantern_text.png', height: 30), root_path, class: "logo-text" %>
</div>
</a>
<button class="navbar-toggler" data-toggle="collapse" data-target="#menu">
<span class="navbar-toggler-icon"></span>
</button>
<div id="menu" class="collapse navbar-collapse">
<ul class="navbar-nav navbar-nav-extend ml-auto">
<% if logged_in? %>
<li class="nav-item nav-item-extend"><%= link_to "Lantern画面へ", '#', class: "btn btn-info btn-md btn-extend" %></li>
<li class="nav-item nav-item-extend"><%= link_to "プロフィール", current_user, class: "btn btn-secondary btn-md btn-extend" %></li>
<li class="nav-item nav-item-extend"><%= link_to "ログアウト", logout_path, method: :delete, class: "btn btn-danger btn-md btn-extend btn-logout-extend" %></li>
<% else %>
<li class="nav-item nav-item-extend"><%= link_to "新しくはじめる", signup_path, class: "btn btn-info btn-md btn-extend" %></li>
<li class="nav-item nav-item-extend"><%= link_to "ログイン", login_path, class: "btn btn-info btn-md btn-extend btn-login-extend" %></li>
<% end %>
</ul>
</div>
</nav>
</div>
</header>
@import "bootstrap";
// 中略
// max-width = 768px
@media (max-width: 768px) {
.nav-item-extend {
margin-top: 0.6rem;
}
}
// button
.btn-extend {
width: 8.5rem;
}
// header
.navbar-extend {
margin-bottom: 0;
background-color: $lantern-black;
opacity: 0.9;
ul li a button {
padding: 0.375rem 1.5rem;
}
}
.logo-img:hover {
opacity: 0.6;
}
.logo-text {
padding-top: 2px;
:hover {
opacity: 0.6;
}
}
.nav-item-extend {
> a:not(.btn-extend) {
// text-decoration: none;
&:hover {
opacity: 0.6;
}
}
> a {
margin-left: 1rem;
float: right;
}
}
.nav-link {
color: $lantern-dark-white;
}
// 中略
Bootstrapでは〇〇px以下という指定が表現しづらいです。
その際は、cssで行います。
@media (max-width: 768px) {
// 処理
}
参考になりました↓
Navbar - Bootstrap(公式)
テストを作る
ログイン/ログアウト機能のテストを作ります。
ここでの手順は以下の通りです。
- FactroyBotを導入する
- is_logged_in?メソッドを作る
- ログイン/ログアウト機能のテストを作る
- ヘッダーのボタンのテストを作る
FactroyBotを導入する
テスト用データの作成にFactroyBotを使います。
まず、Gemを追加します。
group :development, :test do
+ gem "factory_bot_rails"
end
$ bundle install
次に、RSpec内で記述が省略できるよう、設定を変更します。
# 中略
# コメントアウトを外す
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
# 中略
RSpec.configure do |config|
# 以下を追加
config.include FactoryBot::Syntax::Methods
# 中略
end
最後に、FactoryBotを生成します。
$ rails g factory_bot:model user
以上で、FactoryBotの導入完了です。
is_logged_in?メソッドを作る
ログインしているかどうかを確かめる、テスト用のメソッドを作ります。
以前、Sessionsヘルパーにlogged_in?メソッドを作りました。
しかし、#9からRSpec上でlogged_in?メソッドが使えなくなります。
理由は、クッキー関連のメソッドが呼び出せないからです。
(クッキーについては、#9で解説します。)
logged_in?はcurrent_userを使用します。
current_userはcookies.signedメソッドを使用します。
このメソッドが使用できません。
テスト環境でcookiesのクラスが異なるためです。
よってcookiesに依存しないis_logged_in?メソッドを、RSpecのヘルパーに作ります。
RSpecのヘルパーをつくるには、spec/support配下にファイルを作成します。
$ mkdir spec/support
$ touch spec/support/application_helper.rb
module ApplicationHelpers
def is_logged_in?
!session[:user_id].nil?
end
end
最後に、先ほどのヘルパーを呼び出せるよう、設定を加えます。
# コメントアウトを外す↓(まだやっていない方)
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
# 中略
RSpec.configure do |config|
# 追加↓
config.include ApplicationHelpers
以上でテストに必要なis_logged_in?メソッドが完成です。
参考になりました↓
Railsのインテグレーションテストでcookies.signedを使いたい
RSpecでヘルパーを作成する方法
ログイン/ログアウト機能のテストを作る
ログイン/ログアウト機能のテストを作ります。
ログインしているかどうかは、先ほど作ったis_logged_in?メソッドで確かめます。
require 'rails_helper'
RSpec.describe "UsersLogins", type: :request do
let(:user) { create(:user) }
describe "GET /login" do
context "invalid information" do
it "fails having a danger flash message" do
get login_path
post login_path, params: {
session: {
email: "",
password: ""
}
}
expect(flash[:danger]).to be_truthy
expect(is_logged_in?).to be_falsey
end
end
context "valid information" do
it "succeeds having no danger flash message" do
get login_path
post login_path, params: {
session: {
email: user.email,
password: user.password
}
}
expect(flash[:danger]).to be_falsey
expect(is_logged_in?).to be_truthy
end
it "succeeds login and logout" do
get login_path
post login_path, params: {
session: {
email: user.email,
password: user.password
}
}
expect(is_logged_in?).to be_truthy
delete logout_path
expect(is_logged_in?).to be_falsey
end
end
end
end
参考になりました↓
undefined method `session' for nil:NilClass in RSpec Rails
RSpecでテストを書いていたら別ファイルのhelperメソッドの呼び出しが undefined method になって困った時
RSpecのfeatureテストでsessionを扱う方法
【動画付き】Railsチュートリアルの統合テスト(integration test)は、RSpecのリクエストスペックに置き換えるのがラクです
【RSpec】システムスペックとリクエストスペックはどう使い分けるの?
ヘッダーのボタンのテストを作る
ヘッダーにある動的なボタンのテストを作ります。
ログイン画面のブラウザテストも更新しています。
require 'rails_helper'
RSpec.describe "Logins", type: :system do
let(:user) { create(:user) }
describe "Login" do
context "invalid" do
it "is invalid because it has no information" do
visit login_path
expect(page).to have_selector '.login-container'
fill_in 'メールアドレス', with: ''
fill_in 'パスワード', with: ''
find(".form-submit").click
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
expect(page).to have_selector '.login-container'
fill_in 'メールアドレス', with: ''
fill_in 'パスワード', with: ''
find(".form-submit").click
expect(current_path).to eq login_path
expect(page).to have_selector '.login-container'
expect(page).to have_selector '.alert-danger'
visit root_path
expect(page).not_to have_selector '.alert-danger'
end
end
context "valid" do
it "is valid because it has valid information" do
visit login_path
fill_in 'メールアドレス', with: user.email
fill_in 'パスワード', with: 'password'
find(".form-submit").click
expect(current_path).to eq user_path(1)
expect(page).to have_selector '.show-container'
end
it "contains logout button without login button" do
visit login_path
fill_in 'メールアドレス', with: user.email
fill_in 'パスワード', with: 'password'
find(".form-submit").click
expect(current_path).to eq user_path(1)
expect(page).to have_selector '.show-container'
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" do
visit login_path
fill_in 'メールアドレス', with: user.email
fill_in 'パスワード', with: 'password'
find(".form-submit").click
expect(current_path).to eq user_path(1)
expect(page).to have_selector '.show-container'
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
以上でログイン/ログアウト時のテストが完了です。
備考:ジェネレータの種類を確認する
ジェネレータの種類を一覧できる方法を確認します。
「これって、ジェネレータあったかな?」と思う際に便利です。
$ rails g --help
中略 (例:RSpec)
Rspec:
rspec:controller
rspec:feature
rspec:helper
rspec:install
rspec:integration
rspec:job
rspec:mailer
rspec:model
rspec:observer
rspec:request
rspec:scaffold
rspec:view
今回は以上です。