[Rspec]Factorybotのtransientとevaluatorを使ってテストデータを作る
はじめに
この記事では、FactoryBotを使用して、Railsアプリケーションで効率的なテストデータを生成する方法について紹介します。
FactoryBotのセットアップや基本的な使用方法は、公式ドキュメントを参照ください。
対象読者
- Factorybotを使って、テストデータ生成を簡略化したい人
サンプルで仕様するデータ構造
以下のデータ構造に基づいて、FactoryBotを使ったテストデータ生成の方法を紹介します。
このコードでは、User モデルが複数の Post モデルを持ち、with_posts トレイトを使用して指定された数の投稿を作成します。
# app/models/user.rb
class User < ApplicationRecord
has_many :posts
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
end
基本的なfactoryの作成
まず、単純なファクトリを作成しましょう。
これで、UserとPostのデフォルト値を設定できます。
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { "John Doe" }
email { "john.doe@example.com" }
end
end
# spec/factories/posts.rb
FactoryBot.define do
factory :post do
title { "Sample Post" }
content { "This is the content of the sample post." }
user # 関連先の設定。
end
end
Traitを使用したファクトリの拡張
上記のfactoryでもテストデータを作成することはできますが、traitを使用することで、ファクトリの異なるバリエーションを簡単に作成できます。
例えば、Userの生成時に、Postも一緒に生成したい場合、以下のような定義が可能です。
FactoryBot.define do
factory :user do
name { "John Doe" }
email { "john.doe@example.com" }
end
# 二件のPostを生成する
trait :with_posts do
after(:create) do |user|
create_list(:post, 2, user: user)
end
end
end
このtraitは、以下のように使用します。
create(:user, :with_posts)
Transientを使用したデータ生成
transient属性を使用することで、ファクトリの生成時に動的なデータを提供できます。
例えば、管理ユーザの場合に、ユーザーの名前にサフィックスをつけたい場合は以下のように定義できます。
transientで定義した属性は、他の属性から参照可能です。
FactoryBot.define do
factory :user do
name { "John Doe#{' [admin]' if admin}" }
email { "john.doe@example.com" }
transient do
admin { false }
end
end
# 二件のPostを生成する
trait :with_posts do
after(:create) do |user|
create_list(:post, 2, user: user)
end
end
end
このtransientには、以下のように任意のデータを設定可能です。
user = create(:user, admin: true)
user.name
# => John Doe [admin]
指定しなかった場合は、admin: falseとして処理されます。
user = create(:user) # create(:user, admin: false) と同義
user.name
# => John Doe
TransientとEvaluatorを使用した関連データの生成
上記transientをcallback関数内で参照したい場合、evaluatorを使います。
callback関数のブロック引数(2つ目)でevaluatorを宣言することで、transientの値を参照することができます。
例えば、Userのwith_postsで、生成するpostの数を指定したい場合、以下のように定義できます。
FactoryBot.define do
factory :user do
# 割愛
end
transient do
post_count { 2 }
end
after(:create) do |user, evaluator|
# evaluatorを経由して、transientのpost_countを参照している
create_list(:post, evaluator.post_count, user: user)
end
end
post数のデフォルトは2件なので、transientで指定したpost_count
が未指定の場合は、postが2件生成されます。
create(:user, with_posts)
create(:user, with_posts, post_count: 2)
件数を変更したい場合は、以下のように使います。
create(:user, with_posts, post_count: 1)
関連データ生成のバリエーション
上記の例では、件数を任意に指定できるようにしました。
ですが、場合によっては、「このレコードを子レコードとして登録したい」ということがあります。
その場合は、以下のように子レコードにしたいデータを配列で受け取ることも可能です。
FactoryBot.define do
factory :user do
# 割愛
end
transient do
post_count { 2 }
posts { [create_list(:post, post_count)] }
end
after(:create) do |user, evaluator|
evaluator.posts.each do |post|
user.posts << create(:post)
end
end
end
以下のように、postsを指定します。
post1 = create(:post)
post2 = create(:post)
create(:user, :with_posts, posts: [post1, post2])
上記ではこのような定義を使う旨味をうまく表現できなかったのですが...
データ生成(letとbefore)がspecファイルの上下に散らばっている時などに、上記のようなtraitを使うと一箇所見るだけでデータの関連性がわかるので、可読性が上がるかなと思います。
一方で、上記の例では、transientを2つ定義しており、posts
でpost_count
を参照しています。
そのため、仮にposts
とpost_count
の両方を指定した場合は、posts
の定義が優先されます。
具体的には、以下のようなイメージです。
post = create(:post)
user = create(:user, :with_posts, posts: [post], post_count: 2)
user.posts.size
# => 1
この辺りがわかりづらいかもしれないので、議論の対象になりそうです。
また、callback内でevaluator.posts.each
をしているので、この辺りも議論の対象になりそうです。
おわりに
以上、factroybotの機能を使うことで、関連データを持つモデルのテストデータを効率的に作成する方法をご紹介しました。
テストコード実装を書く際の参考になれば幸いです。
Discussion