假设我有一个模型
user
,它对email
字段有唯一性约束
如果我调用
Factory(:user)
一次一切都很好,但如果我第二次调用它,它将失败并出现“条目已存在”错误。
我目前正在使用一个简单的帮助程序在创建工厂之前搜索数据库中的现有条目...并调用我通过该帮助程序创建的任何工厂。
它有效,但并不完全优雅,考虑到我认为这个问题一定很常见,我猜有更好的解决方案。那么,工厂女孩是否有一种内在的方式去
return_or_create
工厂,而不是仅仅带着create()
冲锋呢?如果没有,大多数人如何避免工厂重复输入?
简单答案:使用factory.sequence
如果您有一个字段需要唯一,您可以在factory_girl中添加一个序列以确保它永远不会相同:
Factory.define :user do |user|
sequence(:email){|n| "user#{n}@factory.com" }
user.password{ "secret" }
end
每次都会增加
n
,以生成唯一的电子邮件地址,例如 [email protected]
。 (有关更多信息,请参阅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(100000000).to_s}@factory.com" }
user.password{ "secret" }
end
注意由于 FactoryGirl 中的冲突(bug?),您必须使用
Random.rand
而不是 rand() [https://github.com/thoughtbot/factory_girl/issues/219](参见此处)。
这使您可以从命令行随意创建用户,无论数据库中是否已存在工厂生成的用户。
使电子邮件测试更容易的可选附加功能
当您进行电子邮件测试时,您通常希望验证特定用户的操作是否触发了向另一个用户发送电子邮件。
您以
Robin Hood
身份登录,发送电子邮件至 Maid Marion
,然后前往您的收件箱进行验证。您在收件箱中看到的内容来自 [email protected]
。那到底是谁?
您需要返回数据库检查电子邮件是否由您期望的人发送/接收。这又有点痛苦。
我喜欢做的是使用工厂用户的名称与随机数相结合来生成电子邮件。这使得检查东西来自谁变得更加容易(并且也使得碰撞几乎不可能)。使用 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]
这是我强制工厂女孩序列中的“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
我发现这是确保测试始终通过的好方法。 否则您无法确定 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
如果您只需要为属性生成几个值,您还可以向 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
注意:这不应该用于很多属性,因为我们跟踪所有先前的字符串(可能的优化)。