web-dev-qa-db-ja.com

Rails:Factory Girlの複製エラーを回避する...私はそれを間違っていますか?

userフィールドに一意性制約があるモデルemailがあるとします。

Factory(:user)を1回呼び出すと問題ありませんが、2回目に呼び出すと、「entry already exists」エラーで失敗します。

私は現在、ファクトリを作成する前にDB内の既存のエントリを検索するために単純なヘルパーを使用しており、そのヘルパーを通じて作成したファクトリを呼び出しています。

それは機能しますが、完全にエレガントではありません。この問題がどれほど一般的であると想定するかを考えると、より良い解決策があると思います。したがって、create()を事前に充電するのではなく、ファクトリガールにreturn_or_createを組み込む方法はありますか?そうでない場合、ほとんどの人はどのようにして工場との重複したエントリを回避しますか?

36
PlankTon

簡単な答え:factory.sequenceを使用してください

一意にする必要があるフィールドがある場合は、factory_girlにシーケンスを追加して、同じにならないようにすることができます。

_Factory.define :user do |user|
  sequence(:email){|n| "user#{n}@factory.com" }
  user.password{ "secret" }
end
_

これは、_[email protected]_などの一意の電子メールアドレスを生成するために、毎回nをインクリメントします。 (詳細は https://github.com/thoughtbot/factory_girl/wiki/Usage を参照してください)

ただし、これはRails.env.developmentで常に優れているとは限りません...

時間が経つにつれ、これは実際には一意のメールアドレスを作成するための最も有用な方法ではないことがわかりました。その理由は、ファクトリはテスト環境に対して常に一意である一方で、開発環境に対して常に一意であるとは限らず、環境を起動および停止するとnが自動的にリセットされるためです。 _:test_ではデータベースがワイプされるためこれは問題になりませんが、_:development_ではしばらく同じデータを保持する傾向があります。

次に衝突が発生し、手動でメールを上書きして、一意であることがわかっている迷惑なものに変更する必要があることに気づきます。

もっと便利:乱数を使う

コンソールから定期的に_u = Factory :user_を呼び出すので、代わりに乱数を生成します。衝突を回避することは保証されていませんが、実際にはほとんど発生しません。

_Factory.define :user do |user|
  user.email {"user_#{Random.Rand(1000).to_s}@factory.com" }
  user.password{ "secret" }
end
_

N.B。 FactoryGirlでの衝突(バグ?)のため、Rand()ではなく_Random.Rand_を使用する必要があります[ https://github.com/thoughtbot/factory_girl/issues/219](see ここに)。

これにより、データベースに工場で生成されたユーザーが既に存在するかどうかに関係なく、コマンドラインから自由にユーザーを作成できます。

メールテストを簡単にするためのオプションの追加

電子メールのテストに入るとき、特定のユーザーによるアクションが別のユーザーへの電子メールをトリガーしたことを確認したいことがよくあります。

_Robin Hood_としてログインし、_Maid Marion_にメールを送信してから、受信トレイに移動して確認します。受信トレイに表示されるのは_[email protected]_からのものです。一体誰なんだ?

データベースに戻って、電子メールが送信者または受信者であると予想した人によって送受信されたかどうかを確認する必要があります。繰り返しますが、これは少し苦痛です。

代わりに、ランダムな番号と組み合わせたFactoryユーザーの名前を使用して電子メールを生成するのが好きです。これにより、発信元の確認がはるかに簡単になります(また、衝突が消失する可能性が低くなります)。 Faker gem( http://faker.rubyforge.org/ )を使用して、取得する名前を作成します。

_Factory.define :user do |user|
  user.first_name { Faker::Name::first_name }
  user.last_name { Faker::Name::last_name }
  user.email {|u| "#{u.first_name}_#{u.last_name}_#{Random.Rand(1000).to_s}@factory.com" }
end
_

最後に、Fakerはメールに適さない名前を生成する場合があるため(Mike O'Donnell)、許容できる文字をホワイトリストに登録する必要があります:.gsub(/[^a-zA-Z1-10]/, '')

_Factory.define :user do |user|
  user.first_name { Faker::Name::first_name }
  user.last_name { Faker::Name::last_name }
  user.email {|u| "#{u.first_name.gsub(/[^a-zA-Z1-10]/, '')}_#{u.last_name.gsub(/[^a-zA-Z1-10]/, '')}_#{Random.Rand(1000).to_s}@factory.com" }
end
_

これにより、_[email protected]_や_[email protected]_などの個性的でユニークなメールが提供されます

72
Peter Nixey

ファクトリーガールシーケンスの「n」をそのオブジェクトのIDと同じにして、衝突を回避するために私が行うことは次のとおりです。

最初に、app/models/user.rbで次のIDを見つける方法を定義します。

def self.next_id
  self.last.nil? ? 1 : self.last.id + 1
end 

次に、spec/factories.rbからUser.next_idを呼び出して、シーケンスを開始します。

factory :user do
  association(:demo)
  association(:location)
  password  "password"
  sequence(:email, User.next_id) {|n| "darth_#{n}@sunni.ru" }
end
11
Jack Desert

私はこれがテストが常に合格することを確実にする良い方法だと思いました。それ以外の場合は、一意のメールを作成する時間の100%を確認できません。

FactoryGirl.define do
  factory :user do
    name { Faker::Company.name }
    email { generate(:email) }
  end
  sequence(:email) do
    gen = "user_#{Rand(1000)}@factory.com"
    while User.where(email: gen).exists?
      gen = "user_#{Rand(1000)}@factory.com"
    end
    gen
  end
end
3
user2593371

属性にいくつかの値を生成するだけでよい場合は、属性に使用された以前の文字列を追跡するメソッドをStringに追加することもできます。その後、次のようなことを行うことができます。

factory :user do
  fullname { Faker::Name.name.unique('user_fullname') }
end

私はこのアプローチをシードに使用しています。シーケンス番号は現実的に見えないため、回避したかったのです。

これを可能にする文字列拡張:

class String
  # Makes sure that the current string instance is unique for the given id.
  # If you call unique multiple times on equivalent strings, this method will suffix it with a upcounting number.
  # Example:
  #     puts "abc".unique("some_attribute") #=> "abc"
  #     puts "abc".unique("some_attribute") #=> "abc-1"
  #     puts "abc".unique("some_attribute") #=> "abc-2"
  #     puts "abc".unique("other") #=> "abc"
  #
  # Internal: 
  #  We keep a data structure of the following format:
  #     @@unique_values = {
  #       "some_for_id" => { "used_string_1" : 1, "used_string_2": 2 } # the numbers represent the counter to be used as suffix for the next item
  #     }
  def unique(for_id)
    @@unique_values ||= {} # initialize structure in case this method was never called before
    @@unique_values[for_id] ||= {} # initialize structure in case we have not seen this id yet
    counter = @@unique_values[for_id][self] || 0
    result = (counter == 0) ? self : "#{self}-#{counter}"
    counter += 1
    @@unique_values[for_id][self] = counter
    return result
  end

end

注意:以前のすべての文字列を追跡するため(最適化が可能なため)、これを多くの属性に使用しないでください。

1
Motine