fv17の日記 - Coding Every Day

Webエンジニアの備忘用ブログです。主にWeb界隈の技術に関して書いています。

RSpec - FactoryBotを導入し、効率良く意味のあるデータを作成する

「Everyday Rails - RSpecによるRailsテスト入門 - 第4章 意味のあるテストデータの作成 」の復習用まとめ。

leanpub.com

本章のゴール

Factory Botに焦点を当て、

  • ファクトリの利点と欠点
  • ファクトリを用いたテスト実装
  • その他、ファクトリのリファクタリングや"関連"を表す高度な使い方
  • ファクトリを使いすぎるリスク

ファクトリ対フィクスチャ

フィクスチャ

Railsのデフォルト機能。しかし、以下の理由で使わない。

  • テストデータがファイルに分割されるため一覧化できず、実装中に覚えておく必要あり
  • データをデータ ースに読み込む際にActive Recordを使わない
  • そのためバリデーションなどが無視され、本番データと一致しない可能性あり

Factory Botのインストール

group :development, :test do
  gem 'rspec-rails', '~> 3.6.0'
  gem 'factory_bot_rails', '~> 4.10.0'
end

ジェネレータでモデル作成時に、自動的にファクトリを作成するように設定

confing/application.rbにおいて、fixtures: false の行を削除する

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

ファクトリの作成

ファイルの作成

bin/rails g factory_bot:model user

spec/factories/users.rbが作成される

データの作成

FactoryBot.define do
  factory :user do
    first_name "Aaron"
    last_name "Sumner"
    email "test@example.com"
    password "test-password"  
  end
end

スペックでFactoryを使う

ファクトリが適切に設定されているかテスト

it "has a valid factory" do
  expect(FactoryBot.build(:user)).to be_valid
end

バリデーションのテスト

正常系

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

異常系

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

該当の項目のみオーバーライド(上書き)することで、ピンポイントで明示的にテストし、分かりやすい。

テスト対象をスペック側でオーバーライドして可読性を高める

# ユーザーのフルネームを文字列として返すこと
it "returns a user's full name as a string" do
  user = FactoryBot.build(:user, first_name: "John", last_name:  "Doe")
  expect(user.name).to eq "John Doe"
end

first_nameとlast_nameをあえて指定してテストしている。これにより自己文書的なテストになり、分かりやすい。

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

問題のあるコード

spec/factories/user.rb
FactoryBot.define do
  factory :user do
    first_name "Aaron"
    last_name "Sumner"
    email "test@example.com"
    password "test-password"
  end
end
spec/models/user_spec.rb
# 複数のユーザーで何かする
it "does something with multiple users" do
  user1 = FactoryBot.create(:user) 
  user2 = FactoryBot.create(:user)
  expect(true).to be_truthy
end
理由

上記コードだと、emailが重複する。
そのためuser2作成時にバリデーションに引っかかる。

シーケンスを使って上記問題を解決

FactoryBot.define do
  factory :user do
    first_name "Aaron"
    last_name "Sumner"
    sequence(:email) { |n| "tester#{n}@example.com" }
    password "test-password"
  end
end

ファクトリで関連を扱う

associationでファクトリを呼び出し、インスタンスを生成

spec/factories/notes.rb

FactoryBot.define do 
  factory :note do
    message "My important note."
    association :project
    user { project.owner }
  end
end

上記のように記述すると、

note = FactoryBot.create(:note)

を実行した場合に、project(やuser)のインスタンスも自動で生成される。

誤った書き方

FactoryBot.define do 
  factory :note do
    message "My important note."
    association :project
    association :user
    # user { project.owner }
  end
end

association :userと記述してしまうと、

と、重複してインスタンスを生成してしまう
 
spec/factories/projects.rb

FactoryBot.define do
  factory :project do
    sequence(:name) { |n| "Project #{n}" } description "A test project."
    due_on 1.week.from_now
    association :owner  # ←ここでuserインスタンス生成。:noteではこのインスタンスを使う
  end 
end

関連付けを別名で行なっている時の注意点

モデルにおいて、他データとの関連付けを下記のように行なっている場合、

app/models/project.rb

class Project < ApplicationRecord
  # ...
  belongs_to :owner, class_name: User, foreign_key: :user_id
  # ...
end

下記のように別名でインスタンスを作成するためには、

factory :project do
  # ...  
  association :owner
end

該当のファクトリにaliasを指定する

factory :user, aliases: [:owner] do
    # ...
end

継承とtraitで複雑なファクトリを簡単にする

属性値が異なるファクトリを複数作成したい場合などに、
継承とtraitを用いることで記述を簡易化し、可読性を高めることができる。

ファクトリを複数作るやり方(重複ありで読みにくい)

締め切りが異なるプロジェクトを作成したい場合、
インスタンスの名称を変更し、クラス名を渡せば良い。

FactoroyBot.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 
  end

  # 昨日が締め切りのプロジェクト
  factroy :project_due_yesterday, class: Project do
    sequence(:name) { |n| "Test Project #{n}" }
    description "Sample project for testing purposes"
    due_on 1.day.ago
    association :owner 
  end
end

しかし、上記の実装だと、重複している箇所が多くDRYになっていない
そのため、継承またはtraitを使って重複を解消する。

ファクトリの継承を使う

継承を使うと class: Project の指定は不要。

FactoroyBot.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

    # 昨日が締め切りのプロジェクト
    factroy :project_due_yesterday do
      due_on 1.day.ago
    end
  end
end

traitを使う

FactoroyBot.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

    # 昨日が締め切り
    trait :due_yesterday do
      due_on 1.day.ago
    end
  end
end

トレイトの使い方は下記のように、引数で渡す。

describe "latestatus" 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

コールバック

ファクトリがオブジェクトをcreateする前後でアクションを追加できる。

trait :with_notes do
  after(:create) { |project| create_list(:note, 5, project: project) }
end

after(:create)の他には、after(:build)やbefore(:create)などもある

ファクトリを安全に使う

不要データを作成したり、パフォーマンス面を考慮し、下記に従う。

  • コールバックを使って関連データを作成する場合、トレイトの中でセットアップする
  • 可能な限り、FactoryBot.createではなくFactoryBot.buildを使う
  • 最初の頃はファクトリを使う必要があるのか?を自問する