• トップ
  • ブログ一覧
  • 【rails】FactoryBotでテストデータを柔軟に作成する
  • 【rails】FactoryBotでテストデータを柔軟に作成する

    はじめに

    今回はテストデータを作成するためのgem「FactoryBot」の使い方を解説したいと思います。
    本記事は、機能面についてを重点的に紹介するため導入方法については省略させていただきます。

    基本的な定義方法

    userモデルが、「name, email」を持つ場合以下のような定義になります。
    データ名と対応する値を「{}」に中に設定します。

    1FactoryBot.define do
    2  factory :user do
    3     name {'田中太郎'}
    4     email {'test@example.com'}
    5  end
    6end

    シーケンス

    emailなどに一意制約がある場合は以下のようにシーケンスを使用することで、毎回異なるemailを作成することができます。

    1sequence :email do |n|
    2  "test#{n}@example.com"
    3end
    4
    5# 結果:test1@example.com
    省略記法
    1sequence(:email) { "test#{_1}@example.com" }
    2
    3# 結果:test1@example.com
    初期値設定
    1sequence(:email, 1000) { |n| "test#{n}@example.com" }
    2
    3# 結果:test1000@example.com

    アソシエーション

    userモデルとpostモデルが「1対多」の関係と仮定します。
    親モデルのuserのfactory名を指定するだけで、関連付けをすることができます。
    postモデルのテストデータを作成すると自動的にuserモデルのテストデータも作成されます。

    1FactoryBot.define do
    2  factory :post do
    3    ...
    4    user
    5  end
    6end
    別の書き方
    1FactoryBot.define do
    2  factory :post do
    3    ...
    4    association :user
    5  end
    6end

    factoryを指定したり、属性を上書きすることもできます。

    1FactoryBot.define do
    2  factory :post do
    3    ...
    4    association :author, factory: :user, name: '鈴木太郎'
    5  end
    6end

    ファクトリーを指定することで、以下のようにモデルの関連付けで、「user」ではなく「author」のように別名で関連付けをしている場合などでも関連付けができるようになります。

    1belongs_to :author, class_name: 'User', foreign_key: 'user_id'

    実際にデータ生成する際にアソシエーションを上書きすることもできます。

    1yamada = build(:user, name: '山田太郎')
    2post = build(:post, author: yamada)

    親モデル生成時に子モデルのデータを同時に生成したい時

    子モデルのデータ生成には、「afterコールバック」が使えます。
    userデータを作成後にコールバック内の処理を実行してくれます。
    その時「第一引数」に「親モデル」「第二引数」で「transient」のデータにアクセスすることができます。
    transient」は色んな値を格納できる便利な変数みたいなイメージで良いかと思います。
    transient」に設定した値に応じて、子モデルのデータを柔軟に作成することができます。

    1FactoryBot.define do
    2  factory :user do
    3    transient do
    4      post_datas {
    5        [
    6          {category: 'sports',     comment: 'スポーツに関するコメントです'},
    7          {category: 'technology', comment: 'テクノロジーに関するコメントです'},
    8          {category: 'music',      comment: '音楽に関するコメントです'},
    9        ]
    10      }
    11    end
    12    
    13    ...
    14    
    15    after(:create) do |user, evaluator|
    16      evaluator.post_datas.each do |data|
    17        create(:post, category: data[:category], comment: data[:comment], author: user)
    18      end
    19    end
    20  end
    21end
    22
    23### 以下のようにuserを作成すると
    24user = FactoryBot.create(:user)
    25### postモデルのデータが作成される
    26user.posts.map(&:category)
    27=> ["sports", "technology", "music"]

    「子モデルが持つデータはデフォルトの内容で良いから、指定の数だけ作成したい」といった場合には「create_list」が使えます。
    第二引数に件数を指定することで、件数分の子モデルのデータが作成されます。

    1after(:create) do |user|
    2  create_list(:post, 3, author: user)
    3end

    またこの後紹介する「trait」にafterコールバックを指定してあげれば「trait」を指定した時のみ子モデルのデータを作成することもできます。

    1trait :posts_create do
    2  after(:create) do |user, evaluator|
    3    evaluator.post_datas.each do |data|
    4      create(:post, category: data[:category], comment: data[:comment], author: user)
    5    end
    6  end
    7end

    trait

    「trait」はコールバック処理や一部のデータの値をラップして、factoryと一緒に呼び出すことでfactoryにtraitで定義した内容を結合する機能を持ちます。
    例えば、以下の例では、「music_category」というtraitで「comment, catogory」を定義しています。

    1FactoryBot.define do
    2  factory :post do
    3    comment {'コメントです'}
    4    category {'カテゴリです'}
    5    association :author, factory: :user
    6
    7    trait :music_category do
    8      comment {'音楽に関するコメントです'}
    9      category {'music'}
    10    end
    11  end
    12end

    以下のように呼び出すことで、factoryで定義した「comment, catogory」をtraitの内容で上書きすることができます。

    1# commentが「音楽に関するコメントです」categoryが「music」のデータが作成される
    2create(:post, :music_category)

    traitは複数呼び出すこともできます。
    これにより、複数のtraitを小刻みに用意してあげれば、自由にデータを組み合わせることが可能になります。

    1FactoryBot.define do
    2  factory :user do
    3    name {'田中太郎'}
    4    sequence :email do |n|
    5      "test#{n}@example.com"
    6    end
    7
    8    trait :yamada do
    9      name {'山田太郎'}
    10      sequence :email do |n|
    11        "yamada#{n}@example.com"
    12      end
    13    end
    14
    15    trait :posts_create do
    16      after(:create) do |user|
    17        create_list(:post, 3, author: user)
    18      end
    19    end
    20  end
    21end

    呼び出し

    1# nameが「山田」のuserが作成され、postモデルのデータが3件作成される
    2create(:user, :yamada, :posts_create)

    factoryの継承でも同じような実装をすることができます。

    1FactoryBot.define do
    2  factory :user do
    3    name {'田中太郎'}
    4    sequence :email do |n|
    5      "test#{n}@example.com"
    6    end
    7
    8    factory :yamada do
    9      name {'山田太郎'}
    10      sequence :email do |n|
    11        "yamada#{n}@example.com"
    12      end
    13
    14      after(:create) do |user|
    15        create_list(:post, 3, author: user)
    16      end
    17    end
    18  end
    19end

    呼び出し

    1# nameが「山田」のuserが作成され、postモデルのデータが3件作成される
    2create(:yamada)

    ただ継承は複雑な条件には向きません。
    なぜ向かないのか例として以下のような条件のデータを作成したい時、継承を使用して定義してみましょう。

    • 山田さんは投稿を持っていない
    • 山田さんは投稿を3件持つ
    • 山田さんは投稿を5件持つ
    • 高木さんは投稿を持っていない
    • 高木さんは投稿を3件持つ
    • 高木さんは投稿を5件持つ
    1FactoryBot.define do
    2  factory :user do
    3    name {'田中太郎'}
    4    sequence :email do |n|
    5      "test#{n}@example.com"
    6    end
    7
    8    factory :yamada do
    9      name {'山田太郎'}
    10      sequence :email do |n|
    11        "yamada#{n}@example.com"
    12      end
    13
    14      factory :yamada_has_three_posts do
    15        after(:create) do |user|
    16          create_list(:post, 3, author: user)
    17        end
    18      end
    19
    20      factory :yamada_has_five_posts do
    21        after(:create) do |user|
    22          create_list(:post, 5, author: user)
    23        end
    24      end
    25    end
    26
    27    factory :takagi do
    28      name {'高木太郎'}
    29      sequence :email do |n|
    30        "takagi#{n}@example.com"
    31      end
    32
    33      factory :takagi_has_three_posts do
    34        after(:create) do |user|
    35          create_list(:post, 3, author: user)
    36        end
    37      end
    38
    39      factory :takagi_has_five_posts do
    40        after(:create) do |user|
    41          create_list(:post, 5, author: user)
    42        end
    43      end
    44    end
    45  end
    46end

    呼び出し

    1create(:yamada) # 山田さんは投稿を持っていない
    2create(:yamada_has_three_posts) # 山田さんは投稿を3件持つ
    3create(:yamada_has_five_posts) # 山田さんは投稿を5件持つ
    4create(:takagi) # 高木さんは投稿を持っていない
    5create(:takagi_has_three_posts) # 高木さんは投稿を3件持つ
    6create(:takagi_has_five_posts) # 高木さんは投稿を5件持つ

    同じモデルデータなのになんのモデルのデータを作成しているのかよくわからなくなりました。
    テストデータで複数のモデルのデータを作成する時に、このような記述だといちいち定義元見に行かなければ作成しているデータがわからなくなります。
    このように継承の場合記述が冗長になり、呼び出し元のコードの可読性も下がる可能性があります。

    次にtraitを使用する場合も見てみます。

    1FactoryBot.define do
    2  factory :user do
    3    name {'田中太郎'}
    4    sequence :email do |n|
    5      "test#{n}@example.com"
    6    end
    7
    8    trait :yamada do
    9      name {'山田太郎'}
    10      sequence :email do |n|
    11        "yamada#{n}@example.com"
    12      end
    13    end
    14
    15    trait :takagi do
    16      name {'高木太郎'}
    17      sequence :email do |n|
    18        "takagi#{n}@example.com"
    19      end
    20    end
    21
    22
    23    trait :create_three_posts do
    24      after(:create) do |user|
    25        create_list(:post, 3, author: user)
    26      end
    27    end
    28
    29    trait :create_five_posts do
    30      after(:create) do |user|
    31        create_list(:post, 5, author: user)
    32      end
    33    end
    34  end
    35end

    呼び出し

    1create(:user, :yamada) # 山田さんは投稿を持っていない
    2create(:user, :yamada, :create_three_posts) # 山田さんは投稿を3件持つ
    3create(:user, :yamada, :create_five_posts) # 山田さんは投稿を5件持つ
    4create(:user, :takagi) # 高木さんは投稿を持っていない
    5create(:user, :takagi, :create_three_posts) # 高木さんは投稿を3件持つ
    6create(:user, :takagi, :create_five_posts) # 高木さんは投稿を5件持つ

    traitの場合記述がスッキリし、呼び出し側のコードも何のモデルのデータのなにを作成しているのか引数を見ただけでパッとわかります。
    先ほどの以下の継承を使用した場合の呼び出し方より格段にわかりやすくなりました。

    1create(:yamada) # 山田さんは投稿を持っていない
    2create(:yamada_has_three_posts) # 山田さんは投稿を3件持つ
    3create(:yamada_has_five_posts) # 山田さんは投稿を5件持つ
    4create(:takagi) # 高木さんは投稿を持っていない
    5create(:takagi_has_three_posts) # 高木さんは投稿を3件持つ
    6create(:takagi_has_five_posts) # 高木さんは投稿を5件持つ

    このように細かいデータの調整はtraitで行った方が良いかと思います。
    継承を使用するのは、ファクトリー間で共通部分が多い時などに使用するのが良いかと思います。
    例えば下記のような「一般ユーザー」と「アドミンユーザー」でデータの共通部分が多い場合などは継承の方が良いかもしれません。

    1FactoryBot.define do
    2  factory :user do
    3    name {'田中太郎'}
    4    sequence :email do |n|
    5      "test#{n}@example.com"
    6    end
    7
    8    factory :admin_user do
    9      admin {true}
    10    end
    11  end
    12end

    最後に

    いかがだったでしょうか?
    FactoryBotは便利な機能が多く、rails歴が長くても意外と知らない機能があったりします。
    今回紹介した機能だけで大概のデータは作成できるんじゃないかと思います。
    ぜひFactoryBotの機能を最大限に活かし、DRYで柔軟なテストデータを作成していきましょう。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    はっと(エンジニア)
    はっと(エンジニア)
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background