FactoryBot入門！RSpecコードを綺麗にする！

# FactoryBot

## fixtures と Factory Bot

fixtures と Factory Bot はどちらもテスト用のデータを作成するためのツール。

- **fixtures**
  - 以下のように yml 形式のファイルにサンプルデータの内容を記述し、 `projects(:rogue_one)` という形で呼び出すことができる。
  - 比較的高速。
  - Rails にデフォルトで設定されたテストツール。
  - テストファイル内にデータを記述することができず、別ファイルの内容を覚えておく必要がある。
  - データを DB に読み込む際に Active Record を使用しない。そのため、モデルのバリデーションが無視される。

```yml:fixtures/projects.yml
death_star:
  name: "Death Star"
  description: "Create the universe's ultimate battle station"
  due_on: 2016-08-29

rogue_one:
  name: "Steal Death Star plans"
  description: "Destroy the Empire's new battle station"
  due_on: 2016-08-29
```

対して、Factory Bot は fixtures と比べるとテスト実行速度が遅いが、上記における問題点を解決でき、テストを綺麗に読みやすく保つことが可能。

## Factory Bot のインストール

Factory Bot を使用するためには、以下の gem を追加して、 `bundle install` を実行する。

```ruby:Gemfile
group :development, :test do
  略

  gem "rspec-rails"
  gem "factory_bot_rails" # ここを追加
end
```

さらに、`config/application.rb` の、 `fixtures: false` の行を削除することで、これからジェネレータを使って作られるモデルに対して、自動的にファクトリを作成するように Rails を設定できる。

```ruby:config/application.rb
config.generators do |g|
  g.test_framework :rspec,
    # fixtures: false,     <- ここを削除することで、fixtures を使用しないようにする。
    view_specs: false,     # ビューのテストファイルの作成をスキップ
    helper_specs: false,   # ヘルパーのテストファイルの作成をスキップ
    routing_specs: false   # ルーティングのテストファイルの作成をスキップ
end
```

## アプリケーションにファクトリを追加

factory_bot_rails gem によって新しいファクトリを作成するためのコードを生成可能となる。以下のコマンドで spec ディレクトリ配下に factories ディレクトリが作成され、その中に `user.rb` ファイルが作成される。

```bash
$ bin/rails g factory_bot:model user
```

```rb:spec/factories/user.rb
FactoryBot.define do
  factory :user do

  end
end
```

作成されたファイルに以下のような`{}`を用いた記法で属性を追加できる。

```rb:spec/factories/user.rb
FactoryBot.define do
  factory :user do
    first_name { "Aaron" }
    last_name  { "Sumner" }
    password { "dottle-nouveau-pavilion-tights-furze" }
  end
end
```

こうすると、テスト内で `FactoryBot.create(:user)` と書くだけで、簡単に新しいユーザーを作成できる。

```rb:spec/models/user_spec.rb
require 'rails_helper'

describe User do
  it "has a valid factory" do
    expect(FactoryBot.build(:user)).to be_valid # buildの場合はインスタンス化のみ行い、DBに保存しない。
  end

  # 作成したファクトリの属性のデータをオーバーライドすることもできる
  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
end
```

::: note info
FactoryBot.build を使うと新しいテストオブジェクトをメモリ内に保存。FactoryBot.create を使うとアプリケーションのテスト用データベースにオブジェクトを永続化。
:::

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

例えば、複数のユーザーデータを用意するテストケースの場合、email などのユニーク制約を持つ属性に固定の文字列の設定していると、ユニーク制約違反が発生してテストが失敗してしまう。
このような場合、Factory Bot ではシーケンスを使ってこのようなユニークバリデーションを持つフィールドを扱うことができる。シーケンスはファクトリから新しいオブジェクトを作成するたびに、カウンタの値を 1 つずつ増やしながら、ユニークにならなければいけない属性に値を設定する。

```rb:spec/factories/user.rb
FactoryBot.define do
  factory :user do
    first_name { 'Aaron' }
    last_name  { 'Sumner' }
    sequence(:email) { |n| "tester#{n}@example.com" } # tester1@example.com、tester2@example.com というように、ユニークなデータを作成できる。
    password { 'dottle-nouveau-pavilion-tights-furze' }
  end
end
```

## ファクトリで関連を作成

以下のように各ファクトリに`association`を追加することで、ファクトリを使って関連を作成できる。

```rb:spec/factories/note.rb
FactoryBot.define do
  factory :note do
    message { "My important note." }
    association :project # 関連のprojectを作成 -> 下記のproject.rbでさらに関連のownerを作成
    association :user # 関連のuserを作成する
  end
end
```

```rb:spec/factories/project.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のエイリアス
  end
end
```

上記の`spec/factories/project.rb`では、`association :owner` という記述があるが、これは `spec/factories/user.rb` で定義されたエイリアスでファクトリを参照している。

```rb:spec/factories/user.rb
FactoryBot.define do
  factory :user, aliases: [:owner] do # エイリアスを設定
    first_name { "Aaron" }
    last_name { "Sumner" }
    sequence(:email) { |n| "tester#{n}@example.com" }
    password { "dottle-nouveau-pavilion-tights-furze" }
  end
end
```

しかし、上記のような関連の作成をすると、`note.project.owner` と`note.user` が順番に作成され別々のデータとして作成される。
このような場合に両者を一致させたいなら、以下のように記述する必要がある。

```rb:spec/factories/note.rb
FactoryBot.define do
  factory :note do
    message { "My important note." }
    association :project
    user { project.owner }
  end
end
```

## ファクトリの重複をなくす継承と trait

Factory Bot では同じ型を作成するファクトリを複数定義することもできる。その際はそのファクトリを使って作成するインスタンスのクラス名と、既存のファクトリと異なるインスタンスの属性値(この例でいうと due_on 属性の値)も指定する。

```rb:spec/factories/project.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
  end

  # 昨日が締め切りのプロジェクト
  factory :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

  # 今日が締め切りのプロジェクト
  factory :project_due_today, class: Project do
    sequence(:name) { |n| "Test Project #{n}" }
    description { "Sample project for testing purposes" }
    due_on { Date.current.in_time_zone }
    association :owner
  end
end
```

しかし、上記のようなファクトリには大量の重複の記述があり、メンテナンス性が悪い。そのため、以下のどちらかのテクニックを用いて同じ型を持つファクトリを簡潔に記述すると良い。

### 継承

以下のように、継承元のファクトリ(project)を作成し、そのファクトリの入れ子に継承先のファクトリ(project_due_yesterday, project_due_today, project_due_tomorrow)を作成することで、更新したい属性のみを記述するだけで良くなり重複をなくすことができる。
また、 spec ファイルでも名前を呼び出すのみで、ファクトリの作成が可能。

```ruby
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

    # 今日が締め切りのプロジェクト
    factory :project_due_today do
      due_on { Date.current.in_time_zone }
    end

    # 明日が締め切りのプロジェクト
    factory :project_due_tomorrow do
      due_on { 1.day.from_now }
    end
  end
end

# 呼び出し方
FactoryBot.create(:project_due_yesterday)
FactoryBot.create(:project_due_today)
FactoryBot.create(:project_due_tomorrow)

```

### trait

trait を使用する場合は、基本のファクトリ(project)で属性値の集合を定義するという考え方となる。
それぞれの trait に更新したい属性を記述し、ファクトリ作成時に trait 名を一緒に呼び出すことで、その属性値を上書きすることができる。
trait の利点は複数を組み合わせて複雑なオブジェクトを構築することが可能なこと。

```ruby
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
  end

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

  # 締め切りが今日
  trait :due_today do
    due_on { Date.current.in_time_zone }
  end

  # 締め切りが明日
  trait :due_tomorrow do
    due_on { 1.day.from_now }
  end
end

# 呼び出し方
FactoryBot.create(:project, :due_yesterday)
FactoryBot.create(:project, :due_today)
FactoryBot.create(:project, :due_tomorrow)
```

## コールバック

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

```ruby
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

    # メモ付きのプロジェクトをコールバックで作成
    # create_list は、指定した数のモデルを作成できる。
    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
```