Rails: Во избежание ошибок дублирования в Factory Girl ... я делаю это неправильно? - PullRequest
36 голосов
/ 16 августа 2011

Предположим, у меня есть модель user, которая имеет ограничение уникальности для поля email

Если я позвоню Factory(:user), как только все будет хорошо, но если я позвоню еще раз, произойдет сбой с ошибкой "запись уже существует".

В настоящее время я использую простой помощник для поиска существующей записи в БД перед созданием фабрики ... и вызываю любую фабрику, которую я создаю через этого помощника.

Это работает, но не совсем элегантно, и, учитывая, насколько часто я предполагаю, что эта проблема должна быть, я предполагаю, что есть лучшее решение. Итак, есть ли у фабриканта встроенный способ return_or_create фабрике, вместо того, чтобы просто заряжаться вперед с create()? Если нет, то как большинство людей избегают повторяющихся записей на своих фабриках?

Ответы [ 4 ]

71 голосов
/ 17 сентября 2011

Простой ответ: используйте factory.sequence

Если у вас есть поле, которое должно быть уникальным, вы можете добавить последовательность в factory_girl, чтобы убедиться, что она никогда не будет прежней:

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

Это будет увеличиваться n каждый раз, чтобы создать уникальный адрес электронной почты, такой как `user52@factory.com. (Подробнее см. 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. Вы должны использовать Random.rand вместо rand () из-за столкновения (ошибка?) В FactoryGirl [https://github.com/thoughtbot/factory_girl/issues/219](see здесь).

Это освобождает вас от необходимости создавать пользователей из командной строки независимо от того, есть ли уже созданные пользователем фабричные пользователи в базе данных.

Дополнительная опция для упрощения тестирования электронной почты

Когда вы начинаете тестирование электронной почты, вы часто хотите убедиться, что действие определенного пользователя вызвало отправку электронного письма другому пользователю.

Вы авторизуетесь как Robin Hood, отправляете электронное письмо на Maid Marion и затем входите в свой почтовый ящик, чтобы проверить это. То, что вы видите в своем почтовом ящике, это что-то из user_842@factory.com. Кто это, черт возьми?

Вам нужно вернуться в свою базу данных, чтобы проверить, было ли электронное письмо отправлено / получено кем бы вы ни ожидали. Опять это немного больно.

Вместо этого мне нравится генерировать электронную почту, используя имя фабричного пользователя в сочетании со случайным числом. Это значительно упрощает проверку того, от кого что-то происходит (а также делает столкновения невероятно маловероятными). Используя гем Faker (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 иногда генерирует имена, которые не подходят для электронной почты (Майк О'Доннелл), нам нужно внести в белый список допустимые символы: .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

Это дает нам привлекательные, но уникальные электронные письма, такие как robin_hood_341@factory.com и maid_marion_10@factory.com

11 голосов
/ 15 сентября 2012

Вот что я делаю, чтобы заставить 'n' в моей фабричной последовательности быть такой же, как и у этого объекта, и таким образом избежать столкновений:

Сначала я определяю метод, который находит следующий идентификатор в app / models / user.rb:

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

Затем я вызываю User.next_id из spec / factories.rb, чтобы запустить последовательность:

factory :user do
  association(:demo)
  association(:location)
  password  "password"
  sequence(:email, User.next_id) {|n| "darth_#{n}@sunni.ru" }
end
3 голосов
/ 25 февраля 2016

Я нашел это хорошим способом убедиться, что тесты всегда будут проходить.В противном случае вы не можете быть уверены, что 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
1 голос
/ 27 апреля 2016

Если вам нужно только сгенерировать несколько значений для атрибутов, вы также можете добавить метод String, который отслеживает предыдущие строки, использованные для атрибута. Затем вы можете сделать что-то вроде этого:

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

Я использую этот подход для посева. Я хотел избежать порядковых номеров, потому что они не выглядят реалистично.

Здесь расширение String, которое делает это:

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

Внимание: это не должно использоваться для большого количества атрибутов, так как мы отслеживаем все предыдущие строки (возможна оптимизация).

...