31
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FactoryGirlでユニーク制約がついたモデルとリレーション関係にあるモデルをテストする際に、Validation failed: hoge has already been takenが出た場合の対処法

Last updated at Posted at 2014-12-04

##解決方法
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にはユニーク制約をかけているとする。
スクリーンショット 2014-12-04 9.19.41.png

spec/factories/videos.rb
FactoryGirl.define do
  factory :video do
    country
    event
    name "イケてる動画"
  end
end
spec/models/video_spec.rb
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!'

これはつまり、下図のような挙動になっている。

aa.png

①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メソッド使えば良いということを知った。
具体的には下記のように使う。

spec/factory/country.rb
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という話。

分かってみるとそりゃそうや感あるけど、ちょっと詰まっていたので解決できてよかった。

便利。

31
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?