Rails Diary

プログラミングの学習記録です。

FactoryBot 関連データの作成

FactoryBotにおけるアソシエーション

belongs_toとhas_many ※ 解釈が合っているか分からない
  • 所属(belongs_to)先を記載する場合はFactoryBotにassociation: author(associationは省略可)みたいに記載すればいい
  • has_manyの場合はassociationによる記載が使えないのでtraitやコールバックを用いて、後から関連データを入れ込む形
FactoryBot.define do
  factory :article do
    sequence(:title, "title_1")
    sequence(:slug, "slug_1")
    # belogns_toのリレーションのみ以下の記載が可能?
    category
    author
    # has_manyリレーションではassciationは使えないので、authorやtagの場合はtraitとコールバックであるafter(:create)を使う
  end

transientとコールバック after build

callback…特定の瞬間に呼び出されるメソッド

# articleのFactory続き
# traitを用いてhas_manyリレーションを入れ込む
trait :with_author do
  transient do
    sequence(:author_name, 'test_author_name_1')
    sequence(:tag_slug, 'test_author_slug_1')
  end 

  after(:build) do |article, evaluator|
    article.author = build(:author, name: evaluator.author_name, slug: evaluator.tag_slug)
  end
end
  • transientは実際に作成するデータと直接関係がない変数を定義する機能
  • モデルの属性以外のデータをファクトリに含めることができる
  • 作成時に挙動を変更するためのフラグや追加データとして利用する

■ after(:build)は何をしているのか。evaluatorって何者?

コールバックを使うことで生成したインスタンスがcreate,buildされたイベントの直後に自由にインスタンスを修正できる。

  • つまりafter(:build) do |article, evaluator|というブロックでは、コールバックによってarticleインスタンスが作られた後に、article.authorの値に修正が加えられている
after(:build) do |article, evaluator|
  article.author = build(:author, name: evaluator.author_name, slug: evaluator.tag_slug)
end

↑割と素直にコードを読んで良いんだなと思った。

  • after(:build) do |article|でarticleインスタンスが作られた後、ブロック内でarticle.authorの名前をtransientで生成したものに修正している
  • 第二引数のevaluatorを渡すことで、transientブロック内の変数にアクセスできる
    →この仕組みは、rails newコマンドでなぜ色んなファイルができるのか?みたいなレベルのお話になってくるので考えなくて良いとのこと◎

参考にしたサイト

FactoryGirlのtransientとtraitを活用する - Qiita

FactoryBot(旧FactoryGirl)で関連データを同時に生成する方法いろいろ - Qiita

factory_bot github アソシエーション

雑メモ帳

まとめ方を気にしている余裕がなくなったので、覚書として簡単にメモしておきます。

雑にどんどん追加していきます。

gitでcommit間の差分を出力する

経緯

課題デバッグでHTMLを不必要にいじって表示が乱れてしまったので、どこが原因でおかしくなったのか調べたい。バグに気がついたのがだいぶ後だったため、過去のコミット間の差分を調べる必要がある。

二つのcommit間の差分を出力する

git logで当該箇所のコミットIDを調べ、下記コマンドで差分をチェックできる。

git diff <コミットID1> <コミットID2>

gitでcommit間の差分を出力する - Qiita

Time.nowとTime.current

結論:Time.currentを使った方がいい

[4] pry(main)> Time.now
=> 2022-04-23 16:22:59 +0900
[5] pry(main)> Time.current
=> Sat, 23 Apr 2022 16:23:04 JST +09:00

Time.nowは環境変数(ENV['TZ]')もしくはシステムのタイムゾーンをもとに現在時刻を取得しており、デフォルト設定で環境変数の値はnil。このためアメリカ時刻になっているシステムのタイムゾーンを参照してしまう。

Time.currentはシステムのタイムゾーンに左右されず、Railsアプリごとにapplication.rbに設定したタイムゾーンを参照してくれるので、コンソール上で確認してみると日本時刻で表示されている。

【Rails】Time.currentとTime.nowの違い - Qiita

DateTime

DateTimeは非推奨らしいので、特別な理由がなければTIimeクラスを使う。

より詳しい記事

RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita

Capybara click_onについて

click_onはclick_buttonとclick_linkのエイリアス

これはもう全部click_onでいいのでは🙄
と思っていたけど下記のような場合はclick_onで上手くクリックすることができないらしい。

#<button class="cssClassName">ボタン名</button>
# このような場合は押せる
click_on "ボタン名"


#<button class="cssClassName"><i xxxx />ボタン名</button>
#<button class="cssClassName">ボタン名(1)</button>
# こんな感じだタグ内の値だとうまくいかない
# click_on "ボタン名"

Rspec Capybaraで実際テストを書いて困ったシチュエーションの解消法 - Qiita

範囲演算子

経緯

昨日(終日)中に公開された記事を取得したい。

下記のように記述したけれど、実際もっとスマートな書き方があるかも知れない。

@articles_published_yesterday = Article.where(published_at: Time.zone.now.yesterday.beginning_of_day..Time.zone.now.yesterday.end_of_day)
  • beginning_of_dayend_of_dayでその日の始まりとその日の終わりを取得
  • .....で範囲を指定できる
  • ..は素直に〇〇から〇〇まで
  • ...の場合は終端を含まないSQLが発行されている

ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法|TechRacho by BPS株式会社

whenverの設定

これに関しては理解できていない箇所がかなり多い。

  • cronは.zshとrbenvの環境で動いてくれないらしいので、シェルコマンドの設定と.zshrcとrbenvのPATHを設定で通す必要がある

[Rails4] wheneverを使ってcrontabを管理して、cronを回す - Qiita

remoteの設定で出たエラー

No such remote 'origin'

No such remote 'origin' - Qiita

認可と認証 authorizeメソッド(Pundit)

経緯

課題のアプリ内で使われていたauthorizeメソッドが何のメソッドか分からなかったため少し調べてみた。Punditというgemに搭載されているメソッドらしい。PunditはRubyのgemであり、認可の仕組みを提供してくれる。

Punditをなるべくやさしく解説する - Qiita

認可と認証

よくわかる認証と認可 | DevelopersIO

認証と認可は密接に絡み合っている一方で全く別の概念です。
それでも多くの場合、認可は認証に依存しています

認証(Authenctication)

パスワードや秘密の質問等の、その人だけが知っていることを提示して貰い、通信の相手が誰(何)であるかを確認すること。

認可(Authorization)

とある特定の条件に対して、リソースアクセスの権限を与えること。

★ 認可における「とある特定の条件」が認証にあたることもある

認証せずに認可する例

電車の切符があれば電車に乗れるし、ある部屋の鍵があればその部屋に入ることができる。その人が誰であるかは関係ない。人から切符や鍵を譲り受けていることもある。誰かどうかの認証をせずに電車に乗ったり、部屋に入ったりという認可が得られる。

認証したのに認可しない例

まずないらしい。何のために認証したんだ。(確かに)

しかし OpenID Connect等、認証の委譲が発生するような分散環境においては複雑な事情がありえます。

マクロな視点では「Aシステムがユーザの認証を行い、その事実をBシステムに通知した」という状態において、Aシステムは認証をしたが、認可はしていないことになります。 一方ミクロな視点では「BシステムはAシステムからユーザを認証したことを通知された。だからBシステムは独自で持つリソースへのアクセスを許すことにした」ということがあるかもしれません。恐らく、あるでしょう。 しかしそれはミクロの話で、マクロレベルでは認証しただけ。認可したかどうかは 知らん のであります。

gem: Pundit

rails g pudit:install

上記コマンドによって、app/policies/配下にapplication_policy.rbという認可のルールを記述するファイルが作成される。

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end
end
  • このファイルに定義されているクラスApplicationPolicyを継承して、コントローラごとの認可ルールを記述していく
  • userにはデフォルトでcurrent_userが引数に割り当てられている
  • recordには対応するモデルのインスタンスを手動で割り当てる

例)postという名前のモデルに対してpolicyを作成した場合

# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def update?
    user.admin? or not recorc.published?
  end
end
  • モデル名_policy.rbでファイルを作成
  • モデル名Policyでクラス名を定義
  • def アクション名?で認可ルール(policy)を記述
    →このアクションの返り値によって認可するかどうか」を判断する
def update
  authorize @post
  • authorizeメソッドによって、policyファイルに記述されたdef update?が処理される
  • 引数には対応するモデルオブジェクトを入れる

uuidとは

URLヘルパーにarticleのみ渡したらエラーが出た

describe '記事作成で文章ブロックを追加' do
    let(:article) { create :article }
    context '文章を追加せずにプレビューを閲覧' do
      it '正常に表示される' do
        visit edit_admin_article_path(article) # article.uuidを渡すと通る
        click_link 'ブロックを追加する'
        click_link '文章'
        click_link 'プレビュー'
        switch_to_window(windows.second)
        expect(page).to have_content(article.title), 'プレビューページが正しく表示されていません'
      end
    end
  end
end

https://i.gyazo.com/ac269d6d7d25663761929a802441b648.png

https://i.gyazo.com/c77eb27075c55046f7af67250dfa504d.png

そもそもuuidって何?

Universally Unique Identifier(ユニバーサリー・ユニーク・アイデンティファイア)
重複しない(ことになっている)IDのこと。

uuidを使う理由は?

URLのidから情報を推測しづらくする

いつも利用している整数のidを用いた場合、URLのidから情報が推測しやすくなってしまう。 https://*****/users/1のようなURLがあったとして、末尾のidを変えれば別ユーザーのページに行けそうという考えを持たせる要因になってしまう。

このため550e8400-e29b-41d4-a716-446655440000のような被らない一意の文字列uuidを用いることでURLから情報を推測しづらくすることができる。

注意点

uuidはidの値が乱数になるため、User.firstやUser.lastなどで発行されるSELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1のようなSQLクエリではデータが取得できない。既存のプロジェクトにuuidを導入する場合はorder,first,lastなどが使われていないか確認する。

参考にしたサイト

https://wa3.i-3-i.info/word13163.html

[Rails] モデルのIDにUUIDを使って玄人感を出す | もふもふ技術部

[Rails]idにuuid(乱数)

パンくずリスト

https://i.gyazo.com/284f8651f506c2eb6f257867e329b417.png

Webページの上部(?)に表示される現在地の階層みたいな表示のこと。これがあることによって以下のメリットがある。

  • ユーザーがどのページを読んでいるのか瞬時に分かる
  • Webサイトの検索順位を決める要素を収集するクローラーの巡回を手助けする
  • ↑に付随し、SEO(検索エンジン最適化)に有効

gem: gretelを使用

パンくずリストを簡単に実装できるgem

gem 'gretel'
# bundle install
rails g gretel:install

↑このコマンドにより、config配下にbreadcrumbs.rbというファイルが作成される

# 他のパンくず設定で親ページを指定する際、ビューファイルでパンくずを呼び出す際に使用
crumb :edit_admin_site(パンくず名) do
# リストに表示されるテキストとリンクされるURL
  link '設定', edit_admin_site_path
# 現ページの前のページ(親ページ名)
  parent :admin_dashboard  
end

詳細画面のパンくずリスト

詳細画面なので固有のidが必要

crumb :user_show do |user|
  link "#{user.name}さんの詳細", user_path(user)
  parent :root
end
  • ブロック変数を使う
    →ブロック変数の中身はこのパンくずをビューファイルで呼び出すときや、子となるパンくずうの設定内で定義する
  • リストに表示する文字は式展開を使うことが可能(いつものlink_toと一緒)

ビュー

<% breadcrumb :user_show, @user %>
  • インスタンス変数はコントローラで定義されているものを使用
  • ブロック変数に渡すため、呼び出し時に変数記載
  • application.html.erbへの記載により、各ビューでの呼び出し時には表示させたい箇所に記載する必要がない(後述)

編集ページ

crumb :user_edit do |user|
  link "ユーザー編集"
  parent :user_show, user
end
  • ユーザー編集の親ページである詳細画面にも固有のuser.idが必要なため、userを記載する
<% breadcrumb :user_edit, @user %>

パンくずリストの区切り文字

Home > ○○さんの詳細画面 > ユーザー編集

区切り文字とは文字通り、リストを区切っている「>」のような文字

<!-- app/views/layouts/application.html.erb -->
<body>
  <%= breadcrumbs separator: "区切り文字" %>
  <%= yield %>
</body>
  • パンくずリストは全ページに表示させたいので、application.html.erbの表示させたい箇所に記載
  • 区切り文字はseparator: "区切り文字"のように指定
  • ただし、 「>」はHTMLの閉じタグの意味を持つのでそのまま使うのは良くない
<%= breadcrumbs separator: " &rsaquo; "%>
  • &で始まり、;で終わる書き方でその中に表示させたい文字に対応するコードを記述する

その他

main.content-wrapper
        section.content-header
          h1
            = yield 'content-header'
          == breadcrumbs style: :ol, class: 'breadcrumb'
補足
  • breadcrumbsメソッドがパンくずリストのhtmlを生成している
  • このためビューファイルにbreadcrumbsメソッドを記載しなかった場合、パンくずリストは画面に表示されない
  • またbootstrapのbreadcrumbを適用させるため、class: 'breadcrumb'と記載している
  • bootstrapの公式ドキュメントより、breadcrumbをol要素に適用させているため、style: :olも記載している(デフォルトだとinline)

参考サイト

【Rails】 gretelを使ってパンくずリストを作成しよう | Pikawaka

SEO とは 意味/解説/説明 (エスイーオー) 【Search Engine Optimization, 検索エンジン最適化】 | Web担当者Forum

クローラーとは?検索エンジンの仕組みを解説します!

【Rails】gretelの使い方をざっくりまとめてみた

RSpec作成の流れ

RSpecの作り方の流れをまとめておきたい。
※ 過去のメモと重複箇所多

テストコードはプロダクトコードに比べると絶対的な正解というのがあまりなく、色々な書き方が存在するので、解答例と寸分狂わずみたいなことを目指さなくて良い。

RSpec

スペックファイル作成

bundle exec rails g rspec:system AdminArticlePreviews

ドライバ設定, supportファイルの読み込み設定

① スペックファイルごとにドライバの初期設定が記載されているが、共通項目として別のsupportディレクトリ配下にドライバの設定ファイルを作成する
# spec/support/capybara.rb
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selnium, using: :headless_chrome, screen_size: [1920, 1080]
  end
end
  • ドライバとはテストにおける動作システム。テストの実行環境のこと。デフォルトで設定されているRack::Testは高速だが、JSをテストすることができないので、JSが使えるseleniumドライバを設定している
  • 互換性の問題でヘッドレスブラウザ(テストで使うGUIを持たないブラウザ)にはChromeを採用した方が良いらしい。Chromeを使うためにはwebdriversジェムを追加する。そうすると、依存関係にあるselenium-webdriverも一緒にインストールしてくれるので明示的に書かなくていいとのこと。(詳しくはよくわからない)
② 各スペックがsupport配下のファイル設定を読み込むよう、rails_helper.rbに記載
# 元々コメントアウトになっているものを外せばOK
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

FactoryBotでテストデータを作成

① FactoryBotのファイルを作成
$ bundle exec rails g factory_bot:model user
② テストデータを作成する
FactoryBot.define do
  factory :user do
    sequence(:name, "general_1" } 
    password { "password" }
    password_confirmation { "password" }
    role { :general }

    trait :admin do
      sequence(:name, "admin_1")
      role { :admin }
    end
  end
end
  • sequenceで連番データの作成ができる
  • ユニークなテストデータを作りたい時に使う
  • sequenceはブロックを渡さずに第二引数を渡すとRubyの.nextメソッドが呼び出され、上記のような書き方でも連番が作れる。sequence(:email) { |n| "test#{n}@example.com" }のように連続させたい数字が真ん中に挟まっている場合、.nextが上手く反映されないので大人しくブロックを渡す
  • 管理者権限はtraitを用いて付与
③ FactoryBotの記載省略を設定ファイルに追加

通常、FactoryBot.create(:user)のような記載でダミーデータを作成するが、設定ファイルに下記の記載をすることで先頭のFactoryBotを省略できる

# spec/rails_helper.rb
confin.include FactoryBot::Syntax::Methods

user = create(:user)ないし、let(:user) { create(:user) }みたいな形で作成できる。

ログイン処理を共通項目として切り分ける

ログイン後のテストにおいて、一々ログイン処理をテストに書き込むのはDRYさに欠ける。このため、ログイン処理をモジュールに切り分ける。

① support配下にログイン処理を作成

# spec/support/login_macros.rb
module LoginMacros
  def login_as(user)
    visit admin_login_identifier_path
    click_link 'Login'
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password'
    click_button 'Login'
  end
end

ちなみにmacrosとは

パソコンで、複雑な操作の手順をあらかじめ登録しておき、必要なときに簡単に実行させる機能。マクロ機能。 macro(マクロ)の意味 - goo国語辞書

② 共通項目なので、こちらもrails_helper.rbに読み込みを記載
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods

  # 明示的にLoginMacrosの読み込みを記載
  config.include LoginMacros
end

実際にテストを作成

前述した通り、テストコードに絶対的な正解はないんだそう。ただ現状、自分の乏しい経験値では的確なコードを書くのはかなり難しい。たくさん書いて慣れていくしかないと思う。

require 'rails_helper'

RSpec.describe "AdminArticlesPreviews", type: :system do
  let(:user) { create(:user, :admin)}
  describe 'ログイン後' do
    describe '画像のコンテンツブロック挿入後' do
      context '画像のファイルをアップロードしていない場合' do
        it '記事のプレビューページを閲覧できる' do
          login_as(user)
          click_link '記事'
          click_link '新規作成'
          fill_in 'タイトル', with: 'test'
          click_button '登録する'
          click_link 'ブロックを追加する'
          click_link '画像'
          click_link 'プレビュー'
          switch_to_window(windows.second)
          expect(page).to have_content('test')
        end
      end
    end
  end
end

こんな感じで書いた。 テストを実行すると、

https://i.gyazo.com/8614491183ea222c5a20e3124df429cb.png

エラー発生

https://i.gyazo.com/119b410ef8f2863ce58d095b5f7caff6.png

https://i.gyazo.com/5ea029496fe685193fbbafeea55eb5c1.png

結論、テスト環境にもシードデータを追加することで解決。。

bundle exec rails db:seed_fu RAILS_ENV=test

seed-fuは、既に存在しているが変更したいレコードだけ更新したり、ファイル単位で実行できたり、簡単に書けるようなシンタックスシュガーがあったりど便利です。 railsで初期データを入れる(seed-fuの使い方) - Qiita

rails db:seedを何も考えずに使うと実行する度に同じデータが登録されてしまう。

もしくは、spec/spec_helper.rbに下記を記載

RSpec.configure do |config|
  config.before :suite do
    SeedFu.seed
  end
end

テスト実行の度にシードデータを入れている。

着眼点として

  • current_siteがnilである
  • そもそもSiteのデータ登録が必要?

という思考には至らなかった。もし思い至ってもシードデータをテスト環境に追加しようとは思えなかった。

click_buttonとclick_linkについて

RSpec Capybara href の無い a タグにハマる - かもメモ

自分は検証で調べてbtnならclick_button、見た目がボタンではなくただのリンクならclick_linkみたいな使い方をして来たけれど、aタグはリンクでないとマッチしないらしい。

https://i.gyazo.com/832461d576b7a9b4ea9aec54891c82e9.png

https://i.gyazo.com/46730b7fb7883fea893ad310217994aa.png

見た目がボタンならボタンというわけではないらしい。

参考にしたサイト

【Rails】はじめてのSystemSpec(RSpec) - Qiita

RSpec Capybara href の無い a タグにハマる - かもメモ

railsで初期データを入れる(seed-fuの使い方) - Qiita

RSpecメモ(3)

letを使う

let(:project) { create(:project) }
let!(:task) { create(:task, project_id: project.id) }

FactoryBotのファイルにprojectとのアソシエーションを記載すれば、テストデータ作成の際にproject_idを記載しなくても良いものと思っていたけれど、上手く行かなかったのでproject_idも記載
※ 遅延するletとitの前に作成してくれるlet!の使い所に注意

別ダブを開く時のテスト

projectの詳細画面に遷移し、task一覧へのリンクをクリックすると別タブが開かれる仕様。別タブを開く記載switch_to_window(window.second)がないためテストが失敗していた。

RSpecのテストで別タブをテストする方法 - study-outputの日記

visit project_path(project)
click_link 'View Todos'

switch_to_window(window.second) # 追加

expect(page).to have_content task.title
(省略)

click_linkやclick_buttonの後に記載、最後に開いたタブに移動する場合はwindow.last、次に開いたタブに移動する場合はwindow.secondを記述する。

日付表示

expect(find('.task_list')).to have_content(Time.current.strftime('%Y-%m-%d')) # 削除
expect(find('.task_list')).to have_content(Time.current.strftime('%-m/%d %-H:%M'))  #編集

時刻をformat文字列に従って文字列に変換した結果を返します。 strftimeメソッド

実際のビュー画面とdeadlineの表示がことなっていたので変更

しかしこの書き方だと、Viewファイルの仕様が変わった時にこのままのテストで対応できなくなってしまう模様。

RSpecの日付表示を、viewで使っているメソッドに合わせて修正 - Kuni-Blog、こちらのサイトを同じ状態だった。

viewファイルで使っているヘルパーメソッドを用いてテストも書き換えるのが望ましい。

<!-- app/views/tasks/index.html.erb -->
<td><%= short_time(task.deadline) if task.deadline? %></td>
expect(find('.task_list')).to have_content(short_time(Time.current))

↑のような形で記述する

さらにこのままだとshort_timeを使ったことでNo methodエラーが出てしまうので、rails_helper.rbにApplicationHelperの読み込みを記載する。

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include ApplicationHelper
end
この記載により、RSpecでもヘルパーを用いることができる。