夜行月報

“夜更かし”から“新しい”をみつけるためのブログ

Railsプロジェクトで、FactoryBotを用いたテストデータを作成する方法

FactoryBot RailsでRpsecを用いたテストを行う場合、FactoryBotを使ってテストデータの生成を行うことがよくあるかと思います。 その際、どうすれば効率よく適切なテストデータを作成できるのか、という点について勉強したことをメモしていこうと思います。 [:contents]

前提

各種バージョン

  • Rails 5.1.6
  • rspec-rails 3.8.0
  • factory_bot_rails 4.11.1

以下実例として、名前や年齢などの属性を持つUserモデルをテスト対象として進める。

usersテーブル / userモデル

# テーブル構成

create_table "users", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
  t.str ing "name", null: false
  t.string "email", null: false
  t.integer "old", null: false
  t.string "language", null: false
  t.boolean "loged_in", default: false
  t.boolean "admin", default: false
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end
# models/user.rb

class User < ApplicationRecord
  validates :name, :email, :old, :language, presence: true
  validates :loged_in, :admin, inclusion: { in: [true, false] }
  validates :old, numericality: { greater_than: 0, less_than: 120 }
end

FactoryBotによるテストデータの作成方法

単純なテストデータ作成

ファクトリーの定義

# factory/users.rb
# 以下のように記述することで、静的なテストデータを定義できる
FactoryBot.define do
  factory :user do
    name { 'Tester' }
    email { 'example@email.com' }
    old { 20 }
    language { 'Japanese' }
    loged_in { false }
    admin { false }
  end
end

※属性をブロックで囲むと、その内容はインスタンス生成時に初めて(動的に)評価される。(これを遅延評価属性という)

favtory_bot5.0では、この動的なパラメータが標準となり、ブロックを用いない静的なパラメータを用いると警告がでるので注意。

テストデータの作成方法

テストデータを作成、使用するには以下のように記述する。

# テストDB登録
create(:user)

# 作成されるテストデータ(idとタイムスタンプ以外は同じものが毎回生成される)
# <User id: 1, name: "Tester", email: "example@email.com", old: 20, language: "Japanese", loged_in: false, admin: false, created_at: "2018-11-04 15:06:48", updated_at: "2018-11-04 15:06:48">

# DB登録はせずにBuildまで
build(:user)

# 属性を上書きしてDB登録
create(:user, name: 'taro')

# <User id: 1, name: "taro", email: "example@email.com", old: 20, language: "Japanese", loged_in: false, admin: false, created_at: "2018-11-04 15:06:48", updated_at: "2018-11-04 15:06:48">

テストでの使用例

# user_spec.rb

RSpec.describe User, type: :model do

  describe 'User' do
    describe 'validation' do
      context 'correct params' do
        # テストデータ作成(DB登録)
        let(:user) { create(:user) }

        it 'is valid' do
          # 上記テストデータの場合、以下のテストはパスする
          expect(user).to be_valid
        end
      end
    end
  end
end

一度に複数のテストデータを作成する(create_list)

# factory/users.rb

#createやbuildに"_list"をつけ、第二引数に任意の数を指定する
# 以下では、10のテストデータをレコードに登録している
users = create_list(:user, 10)

# 特定の属性を上書きして、複数のオブジェクトを作成することもできる
login_users = create_list(:user, 10, loged_in: true)

他の属性に依存する属性を定義する

# factory/users.rb

FactoryBot.define do
  factory :user do
    name { 'Tester' }
    # 以下では、ユーザー名を頭につけたemailアドレスを生成している
    email { "#{name}@email.com"}
    old { 20 }
    language { 'Japanese' }
    loged_in { false }
    admin { false }
  end
end

連番の重複しないデータを定義する(sequence)

# factory/users.rb

# sequenceで連番を定義する
sequence :email do |n|
  "example#{n}@email.com"
end

# 使い方
FactoryBot.define do
  factory :user do
    name { 'Tester' }
    email
    old { 20 }
    language { 'Japanese' }
    loged_in { false }
    admin { false }
  end
end
---------------------------
# テストデータを作成する度オートインクリメントされる
p create(:user).email # => example1@email.com
p create(:user).email # => example2@email.com


# 連番の初期値も設定できる
sequence(:email, 5) do |n|
  "example#{n}@email.com"
end

属性のグループ化(trait)

"トレイ"と読む。 テストしなければならない対象の属性を、想定されるパターンごとに生成し、柔軟に付け替えることができる。 trait単体では使用できないので注意。

## factory/users.rb

FactoryBot.define do
  factory :user do
    name { 'Tester' }
    email { 'example@email.com' }
    old { 20 }
    language { 'Japanese' }
    loged_in { false }
    admin { false }

    trait :admin_user do
      admin { true }
    end

    trait :already_loged_in do
      loged_in { true }
    end

  end
end

----------------------------------
# 使い方
let(:admin_user) { create(:user, :admin_user, :already_loged_in) }
p admin_user.admin # => true
p admin_user.loged_in # => true

traitsを使えば、複数のtraitを纏めることができる。

# factory/users.rb

factory :user do
  name { 'Tester' }
  email { 'example@email.com' }
  old { 20 }
  language { 'Japanese' }
  loged_in { false }
  admin { false }

  trait : do
    admin { true }
  end

  trait :loged_in do
    loged_in { true }
  end

  # traitで未指定の属性は、親(ここではser)の値を継承する
  factory :login_admin_user, traits: [:admin, :loged_in]
end

------------------------------------------------
# 使い方
let(:admin_user) { create(:login_admin_user) }
p admin_user.admin # => true
p admin_user.loged_in # => true

このようにtraitを使用することで、テストデータをラベリングできる。 それによって「何をテストするためのデータなのか」「テストデータがどんな状態なのか」が分かりやすくなる。

関連するモデルを同時に作成する

クラス定義

実例として、Userモデルに基づくPostモデルを作成(アトリビュート等は省略)し、以下のようにリレーションを記述しておく。

# models/user.rb
class User < ActiveRecord::Base
  has_many :posts
end

# models/post.rb
class Post < ActiveRecord::Base
  belongs_to :user
end

has_manyの関連

# factory/users.rb

FactoryBot.define do
  factory :user do
    name { 'Tester' }
    email { 'example@email.com' }
    old { 20 }
    language { 'Japanese' }
    loged_in { false }
    admin { false }

    # 以下のように書くと、上で定義しているuserに対して関連するpostを生成してくれる
    after(:create) do |user|
      create(:post, user: user)
    end

    # create_listを用いると、関連する複数のpostを生成してくれる(ここでは5つの投稿を生成)
    after(:create) do |user|
      create_list(:post, 5, user: user)
    end
  end
end

-----------------------------------
# 使い方
user = create(:user)
p user.posts.first

# ↓↓↓↓↓↓
# <Post id: 1, title: "MyString", content: "MyString", user_id: 1, created_at: "2018-11-08 15:31:45", updated_at: "2018-11-08 15:31:45">

belongs_toの関連

# factory/users.rb

FactoryBot.define do
  factory :post do
    title: { 'sample' }
    content: { 'comment' }
    # ファクトリ内で別のファクトリ呼び出すと、連鎖的にインスタンス生成することができる
    # モデルが互いに関連していた場合は、リレーションが張られる
    user
  end
end
-------------------------------
# 使い方
post = create(:post)
p post.user

# リレーションに基づき連鎖生成されたUserを参照できている
# ↓↓↓↓↓↓
# <User id: 1, name: "Tester", email: "example@email.com", old: 20, language: "Japanese", loged_in: false, admin: false, created_at: "2018-11-04 15:06:48", updated_at: "2018-11-04 15:06:48">

-------------------------------
# 事前に作成していたオブジェクトを関連先にしたい場合は、以下のように記載する
  let(:user) { create(:user) }
  let(:post) { create(:post, user: user) }

traitを使った動的なリレーション定義

traitを用いれば、リレーションに基づいたインスタンスの連鎖生成も自由にコントロールできる

# has_many
FactoryBot.define do
  factory :user do
    name { 'Tester' }
    email { 'example@email.com' }
    old { 20 }
    language { 'Japanese' }
    loged_in { false }
    admin { false }

    # このtraitを使用した時のみ、3つの関連するpostが連鎖生成される
    trait :with_posts do
      after(:create) do |user|
        create_list(:post, 3, user: user)
      end
    end
  end
end

-------------------------------
# belongs_to
FactoryBot.define do
  factory :post do
    title: { 'sample' }
    content: { 'comment' }
    
    # このtraitを使用した時のみ、親となるuserオブジェクトが連鎖生成される
    trait :with_parent_user do
      user
    end
  end
end

一時変数を利用する(transient)

transientは、Factory内部で使用する一時的な変数。 利用することで、動的に属性の定義を行うことができる。

FactoryBot.define do
  factory :post do
    transient do
      is_sample { true }
    end

    title: { is_sample ?  'sample' : 'not sample' }
    content: { 'comment' }
    
    # transientで定義した一時変数をファクトリーコールバックの中で使用したい場合
    # ブロック引数の2番目を宣言し、そこからアクセスする必要がある
    after(:create) do |user, evaluator |
      user.content = 'this is sample content' if evaluator.is_sample
    end
  end
end

post_A = create(:post)
p post_A.title #=> 'sample'
p post_A.content #=> 'this is sample content'

# transientの値はテストデータ生成時に引数で指定することで上書き可能
post_B = create(:post, is_sample: false)
p post_B.title #=> 'not sample'
p post_A.content #=> 'comment'

またtransientは、traitと合わせて使うこともできるため、より柔軟なテストデータの生成に使用できる。

おわりに

以上、FactoryBotを用いたテストデータ作成の概要になります。 うーん、たかがテストデータの作成といえども、奥が深い。。。 色々機能は豊富にあるようなので、あとはこれをうまく活用して、柔軟かつ扱いやすいデータ生成を行えるようになれば、テスト自体のコーディングも楽になるんじゃないかなぁ。

指摘等あればどんどんお願いします! ではでは、またまた!