【rails】FactoryBotでテストデータを柔軟に作成する
IT技術
はじめに
今回はテストデータを作成するための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で柔軟なテストデータを作成していきましょう。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
服部晋平と申します! 前職では映像業や配送業に携わっていました。 趣味は、バイクツーリングに行ったり、美味しいラーメン屋巡りです。 未経験という身で入社させていただいたので、人一倍努力して頑張っていきたいと思います!