Try   HackMD

第4章 意味のあるテストデータの作成

この章の主な流れ

  • Factory Botをインストールする
  • ファクトリを追加する
  • シーケンスを使ってユニークなデータを生成する
  • ファクトリで関連を扱う
  • ファクトリ内の重複をなくす(継承/トレイト)
  • コールバックを使う
  • ファクトリを安全に使うには

1. Factory Botをインストールする

Gemfileにgemを追加する。

group :development, :test do gem "rspec-rails" gem "factory_bot_rails" end

ターミナルでbundleコマンドを実行する。

bundle

config/application.rbのfixtures: false, g.factory_bot falseの記述を削除しておく。

config.generators do |g| g.test_framework :rspec, view_specs: false, helper_specs: false, routing_specs: false end

2. ファクトリを追加する

ターミナルで下記コマンドを実行するとファクトリファイルが自動生成される。
(userの部分は作成したいファクトリのモデル名に置き換える)

bin/rails g factory_bot:model user

自動生成されたファイル spec/factories/users.rb にデータを入力する。

FactoryBot.define do factory :user do first_name { "Aaron" } last_name { "Sumner" } email { "tester@example.com" } password { "dottle-nouveau-pavilion-tights-furze" } end end

データを囲んでいる中括弧({ })はRubyのブロックです。FactoryBot 4.11から記法が変わり、データを囲む中括弧が必須になりました。ですので、この中括弧を忘れないようにしてください。


使用例

例1. FactoryBot.build(:user)の形で使用する。

# 有効なファクトリを持つこと it "has a valid factory" do expect(FactoryBot.build(:user)).to be_valid end

ここでは FactoryBot.build を使ったので、新しいユーザーはインスタンス化されるだけで、保存はされません。


例2. FactoryBot.build(:user, first_name: nil)の形で属性をオーバーライドさせる。

# 名がなければ無効な状態であること it "is invalid without a first name" do user = FactoryBot.build(:user, first_name: nil) user.valid? expect(user.errors[:first_name]).to include("can't be blank") end

例3. FactoryBot.createの形でデータベースに保存する。

# 重複したメールアドレスなら無効な状態であること it "is invalid with a duplicate email address" do FactoryBot.create(:user, email: "aaron@example.com") user = FactoryBot.build(:user, email: "aaron@example.com") user.valid? expect(user.errors[:email]).to include("has already been taken") end

このexampleではテストオブジェクトの email 属性が重複しないことを確認しています。これを検証するためには二つ目(訳注: 原文は「二つ目」になっていますが、「一つ目」が正だと思われます)の User がデータベースに保存されている必要があります。


3. シーケンスを使ってユニークなデータを生成する

emailなどテストデータ毎に異なる値を設定したい時、シーケンスを使ってユニークなデータを生成することが出来る。

spec/factories/users.rb

FactoryBot.define do factory :user, aliases: [:owner] do first_name { "Aaron" } last_name { "Sumner" } #ユニークなemail sequence(:email) { |n| "tester#{n}@example.com" } password { "dottle-nouveau-pavilion-tights-furze" } end end

4. ファクトリで関連を扱う

3つのファクトリデータを以下のように作成する。

spec/factories/notes.rb

FactoryBot.define do factory :note do message { "My important note." } association :project #projectとの関連付け user { project.owner } #project.ownerとの関連付け end end

spec/factories/projects.rb

FactoryBot.define do factory :project do sequence(:name) { |n| "Project #{n}" } description { "Sample project for testing purposes" } due_on { 1.week.from_now } association :owner #ownerとの関連付け end

spec/factories/users.rb

FactoryBot.define do factory :user, aliases: [:owner] do #別名ownerを記載 first_name { "Aaron" } last_name { "Sumner" } sequence(:email) { |n| "tester#{n}@example.com" } password { "dottle-nouveau-pavilion-tights-furze" } end end

テストでnoteを生成すると、project, userも同時に生成される。

spec/models/note_spec.rb

# ファクトリで関連するデータを生成する it "generates associated data from a factory" do note = FactoryBot.create(:note) puts "This note's project is #{note.project.inspect}" puts "This note's user is #{note.user.inspect}" end

5. ファクトリ内の重複をなくす

1. ファクトリの継承を使用する

入れ子構造にして継承させる。

spec/factories/projects.rb

FactoryBot.define do factory :project do #親ファクトリ sequence(:name) { |n| "Test Project #{n}" } description { "Sample project for testing purposes" } due_on { 1.week.from_now } association :owner # 昨日が締め切りのプロジェクト factory :project_due_yesterday do #子ファクトリ due_on { 1.day.ago } end end end

以下のように通常の方法で使用できる。

# 締切日が過ぎていれば遅延していること it "is late when the due date is past today" do #ファクトリの作成 project = FactoryBot.create(:project_due_yesterday) expect(project).to be_late end

2. トレイト(trait)を使用する

spec/factories/projects.rb

FactoryBot.define do factory :project do sequence(:name) { |n| "Project #{n}" } description { "Sample project for testing purposes" } due_on { 1.week.from_now } association :owner # 昨日が締め切りのプロジェクト trait :due_yesterday do #トレイト due_on { 1.day.ago } end end

トレイトを使用するようにスペックを修正する。

spec/models/project_spec.rb

describe "late status" do # 締切日が過ぎていれば遅延していること it "is late when the due date is past today" do #トレイトの呼び出し project = FactoryBot.create(:project, :due_yesterday) expect(project).to be_late end end

6. コールバック

コールバックを使うと、ファクトリがオブジェクトをcreateする前、もしくはcreateした後に何かしら追加のアクションを実行できます。また、createされたときだけでなく、buildされたり、stubされたりしたときも同じように使えます。

Factory Botにはこうした処理を簡単に行うための create_list メソッドが用意されています。コールバックを利用して、新しいオブジェクトが作成されたら自動的に複数のメモを作成する処理を追加してみましょう。

spec/factories/projects.rb

FactoryBot.define do factory :project do sequence(:name) { |n| "Project #{n}" } description { "Sample project for testing purposes" } due_on { 1.week.from_now } association :owner # メモ付きのプロジェクト trait :with_notes do after(:create) { |project| create_list(:note, 5, project: project) } end end end

トレイトを使用するようにスペックを作成する。

# たくさんのメモが付いていること it "can have many notes" do #トレイトの呼び出し project = FactoryBot.create(:project, :with_notes) expect(project.notes.length).to eq 5 end

7. ファクトリを安全に使うには

  • ファクトリが必要なことだけを行い、それ以上のことをやっていないことを確認する
  • コールバックを使って関連するデータを作成する必要があるなら、ファクトリを使うたびに呼び出されないよう、トレイトの中でセットアップする
  • 可能な限り FactoryBot.create よりも FactoryBot.build を使う
  • ここでファクトリを使う必要はあるか考える

その他+αなど(皆様もあれば)

・FactoryBotの記述を省略する

rails_helper.rbに設定を追加する。

RSpec.configure do |config| #以下を追加 config.include FactoryBot::Syntax::Methods end

FactoryBot.を省略し、create(:user), build(:user)の形で作成出来る。

[RSpec] FactoryBot 省略の仕方


・ランダムなデータを作成する

emailなどの異なるデータを作成したい時、テキストではシーケンスを使用していたが、Fakerを使用することも出来る。

【Ruby on Rails】FactoryBotとFakerについてまとめ

使用例

FactoryBot.define do factory :user do name { Faker::Name.name } email { Faker::Internet.unique.email } end end
[1] pry(#<RSpec::ExampleGroups::User::Nested::NameEmailPasswordTeam>)> user
=> #<User id: nil, name: "工藤 響", email: "brandie@bernhard-predovic.co", created_at: nil, updated_at: nil>

初期データ投入するseeds.rbでもFakerを使用することが出来る。

【Rails】seeds.rbとFakerを使ってダミーデータ作ってみた


おすすめのgem

モデルのカラム情報をFactoryBot内に記載してくれる。

gem 'annotate'
# == Schema Information # # Table name: users # # id :bigint not null, primary key # access_count_to_reset_password_page :integer default(0) # avatar :string(255) # crypted_password :string(255) # email :string(255) not null ... FactoryBot.define do factory :user do name { Faker::Name.initials(number: 4) } email { Faker::Internet.free_email }

annotateの使い方


シードデータを扱いやすくするSeed Fu

存在しているが変更したいレコードだけ更新したり、ファイル単位で実行できたり、簡単に書けるようなシンタックスシュガーがあったりと便利

環境毎のデータ管理もできる

seed_fuまとめ


sequenceのちょっと違う書き方

sequence(:title, "title_1") # ← これ
sequence(:title) { |n| "title_#{n}" }

sequenceの第二引数でString#nextを末尾の文字に実行される

pry(main)> 'title_1'.next
=> "title_2"

pry(main)> 'user_#{n}@example.com'.next
=> "user_\#{n}@example.con"

pry(main)> 'あ'.next
=> "ぃ"

sequence?

gem 'simplecov', require: false
gem 'rails_best_practices'

gem "sentry-rails"
gem "sentry-ruby"

gem 'meta-tags’