RSpec入門　4.Factory Botでテストデータを作成する

[前回](https://qiita.com/zeisho/items/418aa80b4b64d25d6f1a)の続き

テストが複雑になってもテストデータのセットアップはシンプルにしたい。

そんなときに使えるRubyライブラリとして有名なものにGemの**Factory Bot**があるので、使い方や注意点についてまとめていきます。

#ファクトリとフィクスチャ
Railsでサンプルデータを生成する手段として、フィクスチャと呼ばれる機能がYAML形式のファイルとしてデフォルトで提供されています。フィクスチャは優れた機能ですが、Railsがフィクスチャのデータをデータベースに読み込む際にActive Recordを使わないため、本番環境の挙動とは異なるといった注意すべき点もあります。

ファクトリは簡単にテストデータを生成でき、コードも短く読みやすくすることができますが、生成されるデータを意識して記述しないと、テスト中に予期しないデータが生成されたり、テストの実行が無駄に遅くなったりします。

それぞれの特徴を理解した上で使い分ければ、よりスムーズにテストを書くことができるようになります。

#Factory Botインストール
```:Gemfile
group :development, :test do
  gem "factory_bot_rails"
end
```

```
$ bundle install
```

ついでに、rails generateでモデルを生成する際にファクトリも自動で生成されるように設定していきます。

fixtures: falseを削除するだけです。

```config/application.rb
config.generators do |g|
  g.test_framework :rspec,
  view_specs: false
  helper_specs: false,
  routing_specs: false
end
```

#ファクトリの追加
Factory Botをインストールできたので、Userモデルのファクトリを追加してみましょう。

```
$ rails g factory_bot:model user
```

このコマンドを実行すると、specディレクトリ内にfactoriesという新しいディレクトリが作られ、その中にはusers.rbという名前のファイルが以下のような内容で作られます。

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

ここにテストデータを作っていきます。

```spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name     { "Zeisho" }
    email    { "hoge@hoge.com" }
    password { "hogehoge" }
  end
end
```

ちゃんとテストデータが作れているか、user_spec.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
  end

  #他のスペック群

end
```

ここでは、FactoryBot.buildを使ってユーザーをインスタンス化し有効性をテストしています。前回の記事で記述した有効なユーザーインスタンスのテストよりコンパクトなスペックになりました。

FactoryBotのデータを上書きしてバリデーションエラーのテストを書き換えていきます。

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

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

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

  #メールアドレスがなければ無効な状態であること
  it "is invalid without a email"
    user = FactoryBot.build(:user, email: nil)
    user.valid?
    expect(user.errors[:email]).to include("can't be blank")
  end

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

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

end
```

#シーケンスでユニークなデータを生成する
example内で複数のテストデータを生成する際、

```
FactoryBot.create(:user)
```
を繰り返し使うと、nameやemailなどの属性が全く同じになるため、バリデーションエラーでテストの実行が止まってしまう場合があります。

FactoryBotではシーケンスを使ってユニークバリデーションを持つデータを生成して解決できます。

```spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name             { "Zeisho" }
    sequence(:email) { |n| "hoge#{n}@hoge.com" }
    password         { "hogehoge" }
  end
end
```

上記のようにすることで、ファクトリで新しいユーザーを生成する度にユニークなメールアドレスにすることができます。

#ファクトリで関連を扱う
FactoryBotでは複数モデルのアソシエーション（関連づけ）を意識したデータを生成することもできるので、紹介します。

関連づけをすると、例えばまずは、Userモデル、Projectモデルに属したNoteモデルのデータを作りたいときなどに、Noteモデルのインスタンスを生成すれば、それに紐づいたUser, Projectモデルのデータも自動で生成してくれるようになります。

まずは、Userモデル、Projectモデルに属したNoteモデルのデータを作ります。

```
$ rails g factory_bot:model note
```

```spec/factories/notes.rb
FactoryBot.define do
  factory :note do
    message { "My important note." }
    association :project  #テストデータ projectとの関連づけ
    user { project.owner }  #テストデータ userとの関連づけ
  end
end
```

続いて、Userモデルに属し、Noteモデルを所有しているProjectモデルです。

```
$ rails g factory_bot:model project
```

```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  #owner（所有する）側の関連づけ
  end
end
```

最後にUserにも関連づけの追記をすれば、完了です。

```spec/factories/users.rb
FactoryBot.define do
  factory :user, aliases: [:owner] do
    name             { "Zeisho" }
    sequence(:email) { |n| "hoge#{n}@hoge.com" }
    password         { "hogehoge" }
  end
end
```

#ファクトリの継承
FactoryBotでは、1つのファイル内に複数のデータをすくることもでき、内容の重複する属性は記述を省略できます。

ユーザーに複数のプロジェクトを持たせる場合を例にみていきましょう。

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

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

:projectのブロック内に記述することで、:projectからdue_on以外の属性を継承したデータを生成できます。
テスト内でファクトリのデータを呼び出すときは、ファクトリ名をそのまま指定することで呼び出すことができます。

また、FactoryBotは、入れ子になっていることから:project_due_yesterday, :project_due_today, :project_due_tomorrowが:projectの子ファクトリであると判断するため、**trait**を使ってclass: Projectの指定もなくすことができます。

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

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

traitを使った子ファクトリを呼び出すには、

```
FactoryBot.create(:project, :due_yesterday)
```
のように、**親ファクトリ, 子ファクトリ**とすることで呼び出せます。

#コールバック
コールバックを使うと、ファクトリがオブジェクトをcreateやbuildなどする前後に追加の処理を実行できます。

projectオブジェクトをcreateする際、それに紐づいたnoteを一緒に生成するコールバックを定義してみます。

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

    #メモ付きのプロジェクト
    trait :with_notes do
      after(:create) { |project| create_list(:note, 5, project: project) }
    end
  end
end
```

:with_notesでは、projectオブジェクトをcreateした後に、create_listメソッドを使ってnoteオブジェクトを5つcreateしています。

定義したコールバックは下記のように呼び出すことができます。

```
FactoryBot.create(:project, :with_notes)
```

[続き](https://qiita.com/zeisho/items/344d913d02063660bfe6)