【Factory bot】Rspecでのテストコード作成をもっと楽に！

# はじめに
こんにちは、エンジニア3年目の嶋田です。  
この記事を開いていただきありがとうございます！

私がエンジニアとして働き始めてから、テストの重要性を痛感しています。  
特にRspecを使用したテストは、コードの品質を保つために欠かせません。  
そして、テストデータの生成にはFactory Botが非常に役立ちます。

今回は、Rspecで使用するFactory Botの基本的な使い方や便利な機能についてまとめてみました。  
この記事が皆さんのテストコードの質を向上させる一助となれば幸いです。

より詳しく知りたい方は、以下のページを参照して下さい。  
（※この記事の内容の大半は、以下のページに書かれている内容です）  
[Factory Bot Getting Started](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md)


# 目次
- [Factory Botとは](#Factory-Botとは)
- [Factoryの定義](#Factoryの定義)
- [Factoryの使い方](#Factoryの使い方)
- [traitの使用](#traitの使用)
- [コールバックの使用](#コールバックの使用)
- [Fakerの使用](#Fakerの使用)
- [まとめ](#まとめ)
- [参考文献](#参考文献)

## Factory Botとは
Factory BotはRspec標準で使える"fixture"に代わり、
テストデータの準備をサポートしてくれるライブラリです。

"factory_bot_rails"はRails向けの拡張版のFactorybot

主にRspecと組み合わせて使用され、テストデータを柔軟かつ効率的に作成することができます。

### 主な特徴
- 簡潔なシンタックスでテストデータを定義
- 依存関係を考慮したデータ生成
- traitやコールバックを使った柔軟なデータ生成

## Factoryの定義
まずは、Factoryの基本的な定義方法について説明します。  
以下の例では、`User`モデルのFactoryを定義します。

```ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "John Doe" }
    email { "john.doe@example.com" }
    password { "password" }
  end
end
```

- `FactoryBot.define do`ブロックの中に、`factory :user do`と記述することで`User`モデルのFactoryを定義します。
- 各属性はブロック内で指定し、具体的な値を設定します。
    - この場合、`name`、`email`、`password`がそれぞれ設定されています。

### 基本的なファクトリー定義方法
#### 必要最低限の属性を持ったファクトリーを定義する
インスタンス化するのに必要最低限の属性を持ったファクトリーを各クラスで一つ定義します。  
具体的には、オブジェクト作成時にバリデーションを通すために必要で、且つデフォルト値を持っていない属性を定義します。

```ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "test#{n}@example.com" } # シーケンスを使う
    first_name { "John" }
    last_name { "Doe" }
    date_of_birth { 18.years.ago } # 値を動的に生成する
    organization # Organizationへのアソシエーション
  end
end
```

#### ハッシュを扱う場合
JSONカラムなどを扱う場合は、以下のように{{ key: value }}とする必要があります。

```ruby
# spec/factories/programs.rb
FactoryBot.define do
  factory :program do
    configuration { { auto_resolve: false, auto_define: true } }
  end
end
```

## Factoryの使い方
次に、定義したFactoryを使用してデータを生成する方法を紹介します。  
以下の例は、Rspecのテスト内でFactory Botを使用する方法です。

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

RSpec.describe User, type: :model do
  it "is valid with valid attributes" do
    user = FactoryBot.build(:user)
    expect(user).to be_valid
  end

  it "has a unique email" do
    user1 = FactoryBot.create(:user, email: "unique@example.com")
    user2 = FactoryBot.build(:user, email: "unique@example.com")
    expect(user2).not_to be_valid
  end
end
```

- `FactoryBot.build(:user)`で、データベースに保存されていない`User`オブジェクトを生成します。
- `FactoryBot.create(:user)`で、データベースに保存された`User`オブジェクトを生成します。
    - 必要に応じて属性をオーバーライドすることもできます。
### buildとcreateの使い分け
- `build`：メモリ上にだけインスタンスを生成します。データベースへの影響はありません。
- `create`：インスタンスを生成し、データベースに保存します。これにより、データベースの状態を変更するテストが可能になります。

```ruby
# インスタンスを生成して返す。インスタンスはまだDBに保存されていない。
user = build(:user)

# DBに保存されたインスタンスを返す。
user = create(:user)
```

### build_listとcreate_list
`build_list`、`create_list`を使うと複数のインスタンスを生成できます。

```ruby
users = build_list(:user, 2)
users = create_list(:user, 2)
```

### 属性を指定する
属性を指定することで、特定の条件に合ったインスタンスを生成できます。

```ruby
# factoryとして定義されていなくても、テーブルに存在するカラムを指定可能
user = create(:user, first_name: "Dave", role: "admin")
```

## traitの使用
traitを使用すると、Factoryに特定の状態や属性を追加することができます。以下に、traitを使用した例を示します。

```ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "John Doe" }
    email { "john.doe@example.com" }
    password { "password" }

    trait :admin do
      admin { true }
    end

    trait :age_20 do
      age { 20 }
    end

    trait :age_19 do
      age { 19 }
    end

    trait :like_vegetables do
      like { "野菜" }
    end

    trait :like_fish do
      like { "魚" }
    end
  end
end
```

- `trait :admin do`ブロック内で、`admin`属性を`true`に設定します。
- `age`や`like`の属性もtraitとして定義することで、組み合わせの自由度が高くなります。

### traitのメリット
traitの大きなメリットは、組み合わせの自由度の高さです。必要なテストデータを小さく分けて定義し、必要に応じて組み合わせることで、様々なテストシナリオに対応できます。

例えば、以下のように組み合わせて使用します。

```ruby
it { expect(build(:user, :age_20, :like_vegetables).method_X).to eq("パターン1") }
it { expect(build(:user, :age_20, :like_fish).method_X).to eq("パターン2") }
it { expect(build(:user, :age_19, :like_vegetables).method_X).to eq("パターン3") }
it { expect(build(:user, :age_19, :like_fish).method_X).to eq("パターン4") }
```

このようにすることで、テストデータの意図が明確になり、必要なデータを簡単に組み合わせることができます。

### 具体例
例えば、20歳が閾値のテストがある場合、以下のようにtraitを利用してテストデータを作成します。

```ruby
factory :user do
  name { "taro" }

  trait :age_20 do
    age { 20 }
  end

  trait :age_19 do
    age { 19 }
  end
end
```

この方法で、テストの軸が増えても対応しやすくなります。例えば、20歳以上と未満、好きな食べ物が野菜かそれ以外かの組み合わせで4パターンのテストデータを作成する場合も簡単です。

```ruby
it { expect(build(:user, :age_20, :like_vegetables).method_X).to eq("パターン1") }
it { expect(build(:user, :age_20, :like_fish).method_X).to eq("パターン2") }
it { expect(build(:user, :age_19, :like_vegetables).method_X).to eq("パターン3") }
it { expect(build(:user, :age_19, :like_fish).method_X).to eq("パターン4") }
```

すっきりと意図が明確なテストデータが用意できるようになります。テスト作成者が自由に必要なデータを組み合わせることができます。

もちろん例のような簡単なケースなら、以下のように事前定義不要の可能性もあります。

```ruby
build(:user, age: 20, like: "野菜")
```

しかし、汎用性がありそう、再利用される、初期化が手間、などの場合には、traitでテストデータを用意することも有益です。

## コールバックの使用
コールバックを使用すると、オブジェクトの生成前後に特定の処理を実行できます。以下に、コールバックを使用した例を示します。

```ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "John Doe" }
    email { "john.doe@example.com" }
    password { "password" }

    after(:create) do |user|
      create_list(:post, 3, user: user)
    end
  end
end
```

### callbackとtransientを組み合わせる例
callbackをtransientと組み合わせて使うことで、transientが指定された時に関連付けられたデータを生成します。

```ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    # その他の属性

    transient do
      posts_count { 0 }
    end

    after(:create) do |user, evaluator|
      create_list(:post, evaluator.posts_count, user: user) if evaluator.posts_count.positive?
    end
  end
end
```

### callbackとtraitを組み合わせる例
callbackをtraitと組み合わせて使うことで、traitが指定された時に関連付けられたデータを生成します。

```ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    # その他の属性

    trait :user_with_departments do
      after(:create) do |user, _|
        create(:department, :sales, user: user)
      end
    end
  end
end
```

### 使用例
#### pattern 1 を使う場合
userに紐付いたpostsが、after callback（`after(:create)`）によって生成されます。

```ruby
create(:user, posts_count: 1)
```

#### pattern 2 を使う場合
```ruby
create(:user, :user_with_departments)
```

## Fakerの使用
Fakerを使うと、ランダムなダミーデータを簡単に生成できます。これにより、テストデータに多様性を持たせることができます。

### Fakerのインストール
Gemfileに以下の行を追加し、`bundle install`を実行します。

```ruby
gem 'faker'
```

### Fakerの使用例
以下は、Fakerを使用してユーザーデータを生成する例です。

```ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.email }
    password { Faker::Internet.password(min_length: 8) }
  end
end
```

- `Faker::Name.name`でランダムな名前を生成します。
- `Faker::Internet.email`でランダムなメールアドレスを生成します。
- `Faker::Internet.password(min_length: 8)`でランダムなパスワードを生成します。

## まとめ
以下に、今回説明したFactory Botの主な特徴と使用シナリオをまとめました。

| 機能           | 説明                                                         | 使用シナリオ                                      |
|--------------|------------------------------------------------------------|----------------------------------------------------|
| Factoryの定義  | モデルごとにデフォルトの属性を持つテストデータを定義。              | 基本的なテストデータの生成。                     |
| traitの使用  | 特定の状態や属性を持つデータを追加で定義。                      | 特定の条件下でのテストデータの生成。              |
| コールバックの使用| オブジェクトの生成前後に特定の処理を実行。                     | 関連データの生成や追加のセットアップが必要な場合。 |
| Associationの使用 | 関連するオブジェクトを自動的に生成。                          | モデル間の関係性を考慮したテストデータの生成。      |
| Fakerの使用     | ランダムなダミーデータを生成。                                 | テストデータに多様性を持たせたい場合。              |

Factory Botを使うことで、テストデータの生成が容易になり、テストコードの可読性とメンテナンス性が向上します。皆さんもぜひ、これらのテクニックを活用して、効率的なテストライティングを実現してください。

読んでいただき、ありがとうございました！

## 参考文献
- [Factory Botの公式ドキュメント](https://github.com/thoughtbot/factory_bot)