##解決方法
initialize_with { find_or_create_by } を使うのじゃ。
##環境
- Ruby 2.1.3
- Rails 4.1.6
- RSpec 3.1.0
- FactoryGirl 4.5.0
- databas_cleaner 1.3.0
##現象
FactoryGirlでテスト書いてる時に詰まってしまった。
例えば下図のようなリレーションが貼られていて、videoのテストを書きたい場合。
※ここでcountriesのnameにはユニーク制約をかけているとする。
FactoryGirl.define do
factory :video do
country
event
name "イケてる動画"
end
end
require 'rails_helper'
RSpec.describe Lesson, :type => :model do
it "has a valid factory" do
expect(FactoryGirl.build(:video)).to be_valid
end
end
これを実行すると、
1) Video has a valid factory
Failure/Error: expect(build(:video)).to be_valid
ActiveRecord::RecordInvalid:
Validation failed: Name has already been taken
# ./spec/models/video_spec.rb:5:in `block (2 levels) in <top (required)>'
# -e:1:in `<main>'
というように、テスト失敗する。かなしい。
##原因
FactoryGirl.build(:video)するときに、以下のような流れでcountryを呼んでいるため。
Loading test environment (Rails 4.1.6)
[1] pry(main)> FactoryGirl.build(:video)
(0.1ms) BEGIN
Country Exists (0.3ms) SELECT 1 AS one FROM `countries` WHERE `countries`.`name` = BINARY 'Japan' LIMIT 1
SQL (0.2ms) INSERT INTO `countries` (`created_at`, `name`, `updated_at`) VALUES ('2014-12-04 00:50:39', 'Japan', '2014-12-04 00:50:39')
(0.6ms) COMMIT
(0.1ms) BEGIN
SQL (0.2ms) INSERT INTO `events` (`country_id`, `created_at`, `name`, `updated_at`) VALUES (1, '2014-12-04 00:50:39', 'Oktoberfest', '2014-12-04 00:50:39')
(0.2ms) COMMIT
(0.1ms) BEGIN
Country Exists (0.2ms) SELECT 1 AS one FROM `countries` WHERE `countries`.`name` = BINARY 'Japan' LIMIT 1
(0.1ms) ROLLBACK
ActiveRecord::RecordInvalid: Validation failed: Name has already been taken
from /Users/HeesungLee/.rbenv/versions/2.1.3/lib/ruby/gems/2.1.0/gems/activerecord-4.1.6/lib/active_record/validations.rb:57:in `save!'
これはつまり、下図のような挙動になっている。
①events_idを見て、eventを作りに行くも、eventで指定しているcountries_idがないため、countryをまず作る。
②countris_idを見て、countryを作る。
ここで②番目を実行する際に、ユニーク制約に引っかかる。そりゃそうだなってことで、どうすればよいか調べてみた。
##解決方法
やり方はいくつかある。
ここのサイト(http://d.hatena.ne.jp/a666666/20100402/1270134937) に書かれてるみたいに、countryのnameにシーケンス持たせたり、videoにcountryのインスタンス渡したりとか。
ただ、前者はhas_manyの関係満たせてないし、後者はめんどくさすぎる。(もっと深い構造間になると、めっちゃcreateしてインスタンス渡しまくる必要が出てくる)
何か良い方法ないかなあと考えていたら、find_or_create_byメソッド使えば良いということを知った。
具体的には下記のように使う。
FactoryGirl.define do
factory :country do
name "Japan"
initialize_with { Country.find_or_create_by(name: name)}
end
end
find_or_create_byはRails4.0から登場したメソッドで、検索条件を指定して、初めの1件を取得して、1件もなければ作成する。(http://railsdoc.com/references/find_or_create_by)
今回の場合は、①のときにcreateされて、②のときにfindされるので、OKという話。
分かってみるとそりゃそうや感あるけど、ちょっと詰まっていたので解決できてよかった。
便利。