内部結合を利用したあいまい検索時にsyntax errorで詰まった話

概要

whereメソッドとjoinメソッドを使用して、内部結合を利用したあいまい検索をした時、syntax errorで詰まった話。

失敗時のコード

掲示板に関して、作成ユーザー、掲示板タグ、掲示板タイトル、掲示板コンテンツ(記事内容)で検索を行おうとして以下のような実装をした所、掲示板コンテンツの部分でsyntax errorが表示された。作成ユーザー、掲示板タグはセレクトボックスによる選択、掲示板タイトル、掲示板コンテンツはフリーワード検索で行えるようにしてある。

# models/board.rb

class Board < ApplicationRecord
  belongs_to :user

  has_many :board_tags
  has_many :contents
  has_many :tags, through: :board_tags

  scope :by_user, ->(user_id) { where(user_id: user_id) }
  scope :by_board_tag, ->(tag_id) { joins(:board_tags).where(board_tags: { tag_id: tag_id }) }
  scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }
  scope :body_contain, ->(word) { joins(:contents).where(contents: { 'contents.body LIKE ?', "%#{word}%" }) }
end
# controllers/boards_controller.rb

class BoardsController < ApplicationController

  def index
    @search_boards = SearchBoards.new(search_params)
    @boards = @search_boards.search.order(id: :desc)
  end

  private

  def search_params
    params[:q]&.permit(:title, :body, :tag_id, :user_id)
  end
# forms/search_boards.rb

class SearchBoards
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :user_id, :integer
  attribute :tag_id, :integer
  attribute :title, :string
  attribute :body, :string

  def search
    relation = Board.distinct

    relation = relation.by_user(user_id) if user_id.present?
    relation = relation.by_board_tag(tag_id) if tag_id.present?

    title_words.each do |word|
      relation = relation.title_contain(word)
    end
    body_words.each do |word|
      relation = relation.body_contain(word)
    end

    relation
  end

  private

  def title_words
    title.present? ? title.split(nil) : []
  end

  def body_words
    body.present? ? body.split(nil) : []
  end
end
# views/boards/index.html.slim

.ul.list-inline
  li
    = form_with model: @search_boards, scope: :q, url: boards_path, method: :get, html: { class: 'form-inline' } do |f|
      => f.select :user_id, User.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
      => f.select :tag_id, Tag.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
      .input-group
        = f.search_field :title, placeholder: "タイトル", class: 'form-control'
      .input-group
        = f.search_field :body, placeholder: "本文", class: 'form-control'
        span.input-group-btn
          = f.submit '検索', class: %w[btn btn-default btn-flat]

エラーを見ると、Boardモデルに書いたscope :body_containの部分で構文エラーが発生していたみたい。

SyntaxError - syntax error, unexpected ',', expecting =>
...ents: {'contents.body LIKE ?', "%#{word}%"}) }
...                              ^
/Users/yoshitaka/Desktop/app/models/article.rb:74: syntax error, unexpected ')', expecting end
...nts.body LIKE ?', "%#{word}%"}) }
...                              ^:
  app/models/board.rb:74:in `'
  app/controllers/boards_controller.rb:7:in `index'

原因

何故こうなったかを調べると、内部結合を利用して結合先のテーブルで文字列検索を実行する時は一致検索時とは異なり、.where(結合先のテーブル名: { }の結合先テーブル部分を書く必要がないことが原因だった。
セレクトボックスで一致検索をする場合、内部結合を利用した一致検索を行うコードは以下のように、whereメソッドの引数に「結合先のテーブル名: { カラム名: 値 }」を指定する事で検索できます。

モデル名.joins(:関連名).where(結合先のテーブル名: { カラム名: 値 })

# article_tagsテーブルのtag_idカラムが1のレコードを全て取得
Board.joins(:board_tags).where(board_tags: { tag_id: 1 }) }

しかしフリーワードであいまい検索をする場合、whereの条件部分に.where(結合先のテーブル名.カラム名 LIKE 〜)といったように、結合先のテーブル名を書くことで結合先のテーブルに対して検索を行なってくれます。

# sentencesテーブルのbodyカラムに「hoge」という文字が入っているレコードを全て取得
Board.joins(:contents).where('contents.body LIKE ?', "%hoge%") }

解決方法

なので、whereの後ろに結合先のテーブル名を書かずに文字列検索用のコードを直後に書いたらエラーが解消した。

scope :body_contain, ->(word) { joins(:contents).where(contents: { 'contents.body LIKE ?', "%#{word}%" }) }
=> scope :body_contain, ->(word) { joins(:contents).where('contents.body LIKE ?', "%#{word}%") }

Rakeタスクのテストをする方法

実装したいこと

前回の記事で実装したrakeタスクの内容をRSpecでテストしたい。
備忘録として記事に残してます。

前提

RSpecFactoryBotは既に設定されているものとして記述してます。

実装

1. specディレクトリ配下にrake_helper.rbを作成し、設定を記述する。

require 'rails_helper'
require 'rake'

RSpec.configure do |config|
  config.before(:suite) do
    Rails.application.load_tasks # Load all the tasks just as Rails does (`load 'Rakefile'` is another simple way)
  end
 
  config.before(:each) do
    Rake.application.tasks.each(&:reenable) # Remove persistency between examples
  end
end

before(:suite):テスト実行時、ただ一度だけブロック内の処理を実行します。
Rails.application.load_tasks:lib/tasks配下のすべての.rakeファイルを読み込んでいる。
before(:each):各テストケースの前にブロック内の処理を都度実行します。
Rake.applicationRake::Applicationインスタンスを生成しており、読み込むファイルの拡張子やファイル名を定義している。 また、読み込む際のクラスも指定している。
reenableインスタンスを再度実行できるメソッド。Rakeタスクのテストを実行する場合、各テストケース間の結合性は邪魔になるので、各Rakeタスクを実行した履歴を消去した上で、再度利用できるようにしている。

2. specディレクトリ配下にrake_helper.rbを作成し、設定を記述する。

テストの内容は前回の記事にも記載したように、「「公開待ち」の記事に対して、公開日時が過去になっているものがあれば、状態を「公開」に変更する」タスクを想定してある。

FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "title-#{n}" }
    sequence(:slug) { |n| "slug-#{n}" }
    state { :draft }
  end
end
require 'rake_helper'
 
 describe 'article_state:update_article_state' do
   subject(:task) { Rake.application['article_state:update_article_state'] }
   before do
     create(:article, state: :publish_wait, published_at: Time.current - 1.day)
     create(:article, state: :publish_wait, published_at: Time.current + 1.day)
     create(:article, state: :draft)
   end

   it 'update_article_state' do
     expect { task.invoke }.to change { Article.published.size }.from(0).to(1)
   end
 end

invokeメソッドでRakeタスクを実行してある。他にはexecuteメソッドでも実行できる。

参考記事

RailsでRakeタスクをシンプルかつ効果的にテストする手法 - Qiita

https://kei-p3.hatenablog.com/entry/2016/08/11/232113

Rakeタスクとwheneverを使用して、定期実行したい処理を回す方法

そもそもの定義

rakeタスクとは

Rakeとは:rubyで処理内容を定義できるビルドツールのこと。
Rakeタスクとは:Rakeが実行する処理内容を「Rakeタスク」と呼ぶ。Rakeタスクを定義する場所を「Rakefile」と呼ぶ。

wheneverとは

wheneverとは:cronと呼ばれる、UNIX系のOSでは標準で備わってある仕組みを、rubyの簡単な文法で設定できるようにしたライブラリのこと。
cronとは:「○時になったら○○のコマンドを実行」などといった具合に、定期的にコマンドを実行するためにメモリ場で常に命令を待機しているプロセス(=デーモンプロセス)のこと。

Rakeタスクの実装方法

1. Rakeタスクのファイルを作成

以下のコマンドを実行する

% rails g task <タスクの名前>

コマンドを実行することでlib/tasks/<タスクの名前>.rakeという新しいファイルが生成される。このファイルにRakeタスクを記入していく。

2. タスクに処理を記入する

今回は「「公開待ち」の記事に対して、公開日時が過去になっているものがあれば、状態を「公開」に変更する」といったRakeタスクを作成する。以下が実装したコードになる。

namespace :publish_article do
  desc "記事の公開日が過去の日時になったら、ステータスが「公開待ち」の記事を「公開」にする"
  task change_published: :environment do
    Article.where(status: :publish_wait).find_each do |article|
      if article.published_at <= Time.current
        article.update( state: :published )
      end
    end
  end
end

task change_published:で区切られたブロック内にRakeタスクの内容を記述する。chenge_published:の部分の名前は適切な名前に変更可能です。
:environmentの記述はデータベースに接続する場合必要になる。今回はArticleクラスのモデルに接続する必要があるので記載してある。

Articleモデルのスキーマ情報は以下の通り

# models/article.rb

# == Schema Information
#
#  id           :bigint           not null, primary key
#  author_id    :bigint
#  uuid         :string(255)
#  title        :string(255)
#  description  :text(65535)
#  body         :text(65535)
#  state        :integer          default("draft"), not null
#  published_at :datetime
#  created_at   :datetime         not null
#  updated_at   :datetime         not null
#  deleted_at   :datetime
#
# Indexes
#
#  index_articles_on_author_id     (author_id)
#  index_articles_on_deleted_at    (deleted_at)
#  index_articles_on_published_at  (published_at)
#  index_articles_on_uuid          (uuid)
#

class Article < ApplicationRecord
  belongs_to :author
  
  enum state: { draft: 0, published: 1, publish_wait: 2 }
end

3. 作成したRakeタスクが存在するか確認する

以下のコマンドを実行する

% bundle exec rake -T

# 出力結果の中に2.で作成したRakeタスクの名前(今回だとchange_publishedの部分)が出力されているばOK

・・・
rake publish_article:change_published
・・・

4. Rakeタスクを実行する

% bundle exec rake publish_article:change_published

ログを確認してエラーが発生しておらず、Rakeタスクが実行されていればOK。

wheneverの使用方法

1. wheneverをインストールした後、以下のコマンドを実行する

# Gemfile

gem 'whenever'
=> bundle installコマンドを実行

以下のコマンドを実行することで、config配下にschedule.rbというファイルが生成される。

% bundle exec wheneverize .

2. schedule.rbに設定と実行する処理内容を記載する

今回は「日付が変わる毎に先ほど記述したRakeタスクを実行する」ことにした。以下が実装したコードになる。

# Rails.rootを使用するために必要
require File.expand_path(File.dirname(__FILE__) + '/environment')
# cronを実行する環境変数
rails_env = ENV['RAILS_ENV'] || :development
# cronを実行する環境変数をセット
set :environment, rails_env
# cronのログの吐き出し場所
set :output, "#{Rails.root}/log/cron.log"

every 1.day, at: '0:00' do
  rake 'publish_article:change_published'
end

requireの部分はRails.rootを使用するためのコードです。
wheneverはRailsとは切り離されたもので単なるrubyのファイルとなっており、Railsとは関係のない単なるrubyのファイルの中でRailsのメソッドを使いたい場合、今回のような記述が必要となってきます。

rails_envの部分はENV['RAILS_ENV']で環境を判断し、何も入っていなければ:developmentをrails_envに代入するようにしています。

処理内容の書き方については、今回は「Rakeタスクを毎日実行する」ようにしてあるのでrakeを使用しました。他の書き方については

runner "Rails内のメソッドを実行"
command "bashコマンドを実行"

といったものがある。

3. crontabコマンドでcronを更新する

schedule.rbを変更しただけでは変更は反映されないので、以下のコマンドを実行することでcronにデータを反映させる。

% bundle exec whenever --update-crontab

次にcrontab -lコマンドを実行して、先ほど反映させた定期実行タスクが存在するかを確認できる。

% crontab -l

# Begin Whenever generated tasks for: (Rails_ROOT)/config/schedule.rb at: 2021-05-07 20:06:17 +0900
0 * * * * /bin/bash -l -c 'cd (Rails_ROOT) && RAILS_ENV=development bundle exec rake publish_article:change_published --silent >> (Rails_ROOT)/log/cron.log 2>&1'

# End Whenever generated tasks for: (Rails_ROOT)/config/schedule.rb at: 2021-05-07 20:06:17 +0900

gretelを使用したパンくずリストの作成

1. gretelをインストールした後、以下のコマンドを実行する

# Gemfile

gem 'gretel'
=> bundle installコマンドを実行

以下のコマンドを実行することで、config配下にbreadcrumb.rbというファイルが生成される。

% bin/rails generate gretel:install

2. 設定ファイルを編集する

生成されたファイルを確認すると、以下のようになっている。

# config/breadcrumb.rb

crumb :issues do
  link "All issues", issues_path
end

crumb :issue do |issue|
  link issue.title, issue_path(issue)
  parent :issues
end

:issueの部分はパンくずの名前になる。
他のパンくず設定で親(parentの部分)を定義するときや、以降で設定するviewファイルでパンくずを呼び出す際の名前を設定する。
linkの部分はパンくずリストに表示されるテキストとリンク先を設定する(書き方はRailsのヘルパーメソッドのlink_toと同様)。
URI Patternが/issues/:idのような、issueのidをパスに含める必要がある場合には|issue|のようにブロック変数を渡す必要がある。
parentの部分は現在ここで作ったページの親ページが何かを設定する。今回の場合、:issueだと:issuesが親に相当する。

3. viewファイルを編集する

最初に、パンくずリストを表示させたい箇所に以下のコードを記述する。

<%= breadcrumbs separator: " &rsaquo; " %> 

separator: " &rsaquo; "はパンくずで表示される文字を区切る部分に入る文字を指定する。>だとHTMLタグと認識され、表示されない可能性があるので、特殊文字を使用している。
今回は全画面にパンくずリストを表示させたいのでapplication.html.erbに記述する。

# app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
  ・・・
  </head>
  <body>
    <%= breadcrumbs separator: " &rsaquo; " %> 
    <%= yield %>
  </body>
</html>
end

次に設定ファイルで設定したcrumb名を各viewファイルに指定する。

# app/views/issues/index.html.erb

<%= breadcrumbs :issues %> 
# app/views/issues/show.html.erb

<%= breadcrumbs :issue, @issue %> 

詳細ページでは:issueのcrumbを使用している。
ここで、設定ファイルで設定したcrumb :issueではブロック変数を渡しており、ブロック変数には元となるインスタンスを渡さなければならない。
よって、詳細ページのviewファイルでは@issueインスタンスを渡してある。

公式リファレンス

github.com

erbからslimへのテンプレートエンジンの変更方法&slimの記法まとめ

Slimの導入方法

1. Gemfileにrails-slimを加え、bundle installコマンドを実行

# Gemfile

gem 'slim-rails'
=>bundle installコマンドを実行

2. デフォルトで生成されるviewファイルのテンプレートエンジンをslimに変更することも可能

# config/application.rb

class Application < Rails::Application
  config.generators.template_engine = :slim  
end

基本的な書き方一覧

・HTMLタグの<>や閉じタグが不要

# index.html.erb
<h1>Users</h1>
# index.html.slim
  h1 Users

・classはh1.name, idはp#nameのように、.#で書く。複数のクラスを記載したい場合は.で繋げる必要がある。

# index.html.erb
<h1 class="name">Users</h1>
<h1 class="name email">Users</h1>
<p id="name">Users</p>
# index.html.slim
  h1.name Users
  h1.name.email Users
  p#name Users

・erbの<% %>というruby記法の使用宣言が「-」に変更する

<% if @blogs.present? %>
  <p>在庫あり</p>
<% end %>
- @blogs.present?
  p 在庫あり

・<%= %> というruby記法の使用宣言と出力が、「=」に変更する

<% @blogs.each do |blog| %>
  <p><%= blog.title %></p>
<% end %>
- @blogs.each do |blog|
  p = blog.title

renderyieldは、=ではなく==を使う
==を使用することで、HTMLエスケープを無効にして出力することができる。
一例として、render_to_stringによって返された「画像を表示する文字列」から画像を表示する場合、以下のようなコードなる。

# admin/articles_controller.rb

class Admin::ArticlesController < ApplicationController
  def show
    @article = Article.find_by!(id: params[:article_id])
    @article.body = @article.controller.render_to_string("shared/_media_image", locals: { medium: medium })
  end
end
irb(#<Admin::Articles::PreviewsController:0x00007fe4cd0b4b40>):001:0> @article.body
=> "<div class=\"media-image\"><img src=\"http://localhost:3000/rails/active_storage/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--4420d225322ef11058618687b5a47ec403e7c6c8/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9MY21WemFYcGxTU0lOTVRBeU5IZzNOamdHT2daRlZBPT0iLCJleHAiOm51bGwsInB1ciI6InZhcmlhdGlvbiJ9fQ==--6ec66a681693b203f182cf67cd6ee51b8eb65eca/sample01.jpg\" /></div>"
# articles/show.html.slim

article.article
    == @article.body

viewファイルの== @article.bodyの部分を= @article.bodyとすると、コンソールで表示した<div class= 〜 </div>までのコードを表示することになるが、== @article.bodyとすることでHTMLエスケープを無効にし、画像を表示することができる。

公式リファレンス

詳しい日本語のリファレンスはこちら。

github.com

テスト環境でsorceryを使用したユーザーログイン機能を使用したとき、Userクラスのインスタンスメソッドでハマった話

概要

テスト環境でsorceryを使用してログイン機能を追加した時、user.passwordで詰まった話。

失敗時のコード

以下のような、ユーザーが正常にログインできるかのテストコードを作成した時、テストに失敗した。

# spec/factories/users.rb

FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user_#{n}@example.com"  }
    password { 'password' }
    password_confirmation { 'password' }
  end
end
# spec/system/usersessions_spec.rb

require 'rails_helper'

RSpec.describe "UserSessions", type: :system do
  let(:user) { create(:user) }

  describe 'ログイン前' do
    context 'フォームの入力値が正常' do
      before do
        visit root_path
        click_link "Login"
        fill_in "Email", with: user.email
        fill_in "Password", with: user.password
        click_button "Login"
      end

      it 'ログイン処理が成功する' do
        expect(current_path).to eq root_path
        expect(page).to have_content "Tasks"
        expect(page).to have_content "Login successful"
      end
    end
  end
end

テストに失敗した時のスクリーンショットを見ると、ログインに成功してなかったみたいなのでclick_buttonの上にbinding.irbを挟んでuser.emailuser.passwordを確認してみると、以下のようにuser.passwordnilが返っていた。

irb(#<RSpec::ExampleGroups::UserSessions::Nested::Nested:0x00007f85a4104d08>):001:0> user.email
=> "user_1@example.com"
irb(#<RSpec::ExampleGroups::UserSessions::Nested::Nested:0x00007f85a4104d08>):002:0> user.password
=> nil

原因

何故こうなったかを調べると、usersテーブルにpasswordカラムが存在してないことが原因みたい。
sorceryを使用してログイン機能を追加する場合、usersテーブルにはcrypted_passwordカラムが存在して、仮想的な属性であるpasswordpassword_confirmationを元にcrypted_passwordカラムを作成している。なのでuser.passworduser.password_confirmationを指定してuser.createを実行すると、user.crypted_passwordが作成されてデータベースに保存されるが、仮想的な属性であるpasswordpassword_confirmationは無くなるみたいである。
今回の場合だとFactoryBot.create(:user)を使用すると、FactoryBotで指定したpasswordpassword_confirmationを値を元にuser.crypted_passwordを作成するのでuser.passworduser.password_confirmationは存在しなくなる(nilになる)。よって、ログインに失敗したようである。

解決方法

なので、今回のようにあらかじめ作成しておいたユーザーデータをもとにログインを検証するテストを作成する場合、fill_in "Password"の箇所をFactoryBotでpasswordに指定した"password"を直に書くことでうまくテストが通るようになった。

fill_in "Password", with: user.password
=> fill_in "Password", with: "password"

RSpec、Capybara、FactoryBotを使用したテスト環境の構築・設定方法、及びテストの書き方一例

テスト環境の構築構築方法

1. RSpec、Capybara、FactpryBot、Webdriversのgemをインストール

# Gemfile

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'capybara'
  gem 'webdrivers'
end

=> bundle installコマンドを実行

2. RSpecに必要なディレクトリや設定ファイルを作成する

% bin/rails g rspec:install
=> Running via Spring preloader in process 10358
     create  .rspec
     create  spec
     create  spec/spec_helper.rb
     create  spec/rails_helper.rb

3. 自動で作成されてあるtestディレクトリを削除する

% rm -r ./test

FactoryBotを使用したテストデータの作成方法

# spec/factories/tasks.rb

FactoryBot.define do
  factory :task do

    # 重複したデータの作成を阻止したい場合、シーケンスを使用する。
    sequence(:title) { |n| "title_#{n}" }
    # シーケンスの第二引数で定義した文字の末尾にはnextメソッドが実行されるので、文字列の末尾を変更することで独立したデータを作成する上のようなコードの場合、以下のようにも書ける。
    => sequence(:title, "title_1")
    content { "content" }
    status { :todo }
    deadline { 1.week.from_now }

    # Userモデルのファクトリとアソシエーションを作成する場合、以下のコードを記載
    association :user

  # traitを使用することで元となるデータを継承・オーバーライドして、属性地をグループ化することができる。
    trait :finished_task do
      status { :done }
      completion_date { Time.current.yesterday }
    end
  end
end

以下、SystemSpecの書き方一例

# spec/system/user_spec.rb
require 'rails_helper'

RSpec.describe 'User', type: :system do
  # letは遅延読み込みを実現するメソッドで、変数に値を代入するように使用できる。
  # 以下のコードでは、テストファイル内でuserを呼び出すことでFactoryBot内で作成したuserデータを作成し、データベースに保存できる。
  let(:user) { FactoryBot.create(:user) }

  describe 'ログイン後' do
    before do
      visit root_path
      click_link "ログイン"
      fill_in "メールアドレス", with: user.email
      fill_in "パスワード", with: "password"
      click_button "ログイン"
    end

    describe 'ユーザー編集' do
      before do
        visit users_path
        click_link "ユーザー編集"
      end

      context 'フォームの入力値が正常' do
        before do
          fill_in "メールアドレス", with: user.email
          fill_in "パスワード", with: "update_password"
          fill_in "再確認", with: "update_password"
          click_button "ユーザー更新"
        end

        it 'ユーザーの編集が成功する' do
          expect(current_path).to eq user_path(user)
          expect(page).to have_content user.email
          expect(page).to have_content "User was successfully updated."
        end
      end
    end
  end
end

テスト環境の各種設定

・ヘッドレスドライバを使用して、ブラウザのウィンドウを開くことなくテストを実行する

# spec/spec_helper.rb

require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by(:selenium_chrome_headless)
  end
end

RSpec の出力をデフォルトの形式からドキュメント形式に変更する

# .rspec
# 以下のコードを追加

--format documentation

・テストで使用する共通の処理(login処理など)を記述したサポートモジュールのファイルをspec/support配下に作成し、その配下のファイルを読み込む

# spec/support/login_support.rb

module LoginSupport
  def sign_in_as(user)
    visit root_path
    click_link "Sign in"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
    click_button "Log in"
  end
end
# spec/rails_helper.rb

# 20行目付近にある以下のコードのコメントアウトをはずすことで、spec/support配下のファイルを読み込む

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

# 以下のコードを追加してLoginSupportモジュールをテストで使用できるようにする
RSpec.configure do |config|
  config.include LoginSupport
end

・実行するテストケースを限定する設定

# spec/spec_helper.rb

# 50行付近にある以下のコードのコメントアウトをはずす
RSpec.configure do |config|
  # config.filter_run_when_matching :focus
  => config.filter_run_when_matching :focus
end

FactoryBot.create(:user)FactoryBot.build(:user)をする時、以下の記載をすることでFactoryBot.の部分を省略できる

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end