FactoryBotを使う時に覚えておきたい、たった5つのこと

# 環境

- Rails 7.0.5
- FactoryBot 6.2.1

# はじめに

- ここに書かれている内容は、基礎の基礎ですが、ユースケースの8割はカバーできるのではないかと思います。より詳しく知りたい方は、以下のページを参照して下さい（※ここの内容の大半は、以下のページに書かれている内容です）。

> https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md

- factory_bot 5 より、静的属性は廃止されました。
属性は、常にブロックを付けて宣言する必要があります。

```ruby
# OK
role { "admin" }

# NG
role "admin"
```

> Static attributes (without a block) are no longer available in factory_bot 5.
> https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#static-attributes

## 1. インスタンス化するのに必要最低限の属性を持ったファクトリーを、各クラスで一つ定義する

具体的には、オブジェクト作成時にバリデーションを通すために必要で、且つデフォルト値を持っていない属性を定義します。

> It is recommended that you have one factory for each class that provides the simplest set of attributes necessary to create an instance of that class. If you're creating ActiveRecord objects, that means that you should only provide attributes that are required through validations and that do not have defaults. Other factories can be created through inheritance to cover common scenarios for each class.
> https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#best-practices

## 2. 基本的なファクトリー定義方法

以下の 3パターンを抑える。

- シーケンスを使う（※主にユニーク制約に抵触しないため）
- アソシエーションを定義する
- 値を動的に生成する

```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
  factory :program do
    configuration { { auto_resolve: false, auto_define: true } }
  end
```

## 3. `build`と`create`を使い分ける

- `build`で十分な時は、`build`を使う。
  - `build` インスタンスを生成して返す。インスタンスはまだDBに保存されていない。
  - `create` DBに保存されたインスタンスを返す。

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

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

- `build_list`、`create_list`で複数のインタンスを生成する。
 
```ruby
users = build_list(:user, 2)
users = create_list(:user, 2)
```

- 属性を指定する事も可能。
 
```ruby
# factoryとして定義されていなくても、テーブルに存在するカラムを指定可能（以下の例ではrole）。
user = create(:user, first_name: "Dave", role: "admin")
```

## 4. `callback`を`transient`や`trait`と組み合わせて使うことで、アソシエーション先のデータを作成する

- `callback`を`transient`と組み合わせて使うことで、`transient`が指定された時に関連付けられたデータを生成します（以下、pattern 1）。
- `callback`を`trait`と組み合わせて使うことで、`trait`が指定された時に関連付けられたデータを生成します（以下、pattern 2）。

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

    # pattern 1 (transient を使う例)
    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

    # pattern 2 (trait を使う例)
    trait :user_with_departments do
      after(:create) do |user, _|
        create(:department, :sales, user: user)
      end
    end
  end
end
```

```ruby
# pattern 1 を使う場合
# userに紐付いたpostsが、after callback（※↑の例では`after(:create)`）によって生成される。
create(:user, posts_count: 1)

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

## 5. `trait`を使って条件の異なるファクトリーを定義する

### 例 1
```ruby
factory :story do
  title { "My awesome story" }

  trait :published do
    published { true }
  end

  trait :week_long_publishing do
    start_at { 1.week.ago }
    end_at { 1.week.from_now }
  end

  # traits で複数の trait をまとめることが出来ます
  factory :week_long_published_story, traits: [:published, :week_long_publishing]
end
```

- `build`や`create`を使ってインスタンス生成する時に、`trait`を指定することも可能です。

```ruby
create(:story, :published, :week_long_publishing)
```

### 例 2: アソシエーション先のレコードを作る
```ruby
# factory の定義
factory :story do
  :
  trait :with_author do
    # デフォルト定義。create （or build） する時に author_email が指定されていなければこの値が使われる。
    author_email { 'author@example.com' }
  end

  # story has_one author のアソシエーション
  author { create(:author, email: author_email) }
end
```

```ruby
# factory を利用する
let(:story) { create(:story, :with_author, author_email: 'test@example.com') }
```

# おまけ

「たった5つ」と書いておきながら、2つだけおまけを付けさせて下さい（笑）。

## 不要な変数を定義しない
```ruby
# OK
# organizationインスタンスを生成した後で、userに代入する。 
org = create(:organization)
create(:user, organization: org)

# Better
# organizationインスタンスを別途生成せずに、user生成時に代入する。
# こちらの方がシンプルだが、状況に応じて、リーダビリティのために上記の方法を採っても良い。
create(:user, organization: create(:organization))
```

## has_manyアソシエーションにデータを関連付ける方法（2パターン）
```ruby
# 1
users = create_list(:users, 2)
create(:organization, users: users)

# 2
user1 = create(:user, role: "admin")
user2 = create(:user, role: "sales")
create(:organization, users: [user1, user2])
```

# おまけのおまけ

フリーランスで活動しています。
開発案件（Go、Node、Rails、Blockchain、機械学習など）をお待ちしております。

[職務経歴書（wantedlyへのリンク）](https://www.wantedly.com/users/36882295)