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を用いたテストデータ作成の概要になります。 うーん、たかがテストデータの作成といえども、奥が深い。。。 色々機能は豊富にあるようなので、あとはこれをうまく活用して、柔軟かつ扱いやすいデータ生成を行えるようになれば、テスト自体のコーディングも楽になるんじゃないかなぁ。
指摘等あればどんどんお願いします! ではでは、またまた!