📘

[Rspec]Factorybotのtransientとevaluatorを使ってテストデータを作る

2023/10/25に公開

はじめに

この記事では、FactoryBotを使用して、Railsアプリケーションで効率的なテストデータを生成する方法について紹介します。
FactoryBotのセットアップや基本的な使用方法は、公式ドキュメントを参照ください。
https://github.com/thoughtbot/factory_bot_rails
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md

対象読者

  • 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つ定義しており、postspost_countを参照しています。
そのため、仮にpostspost_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