【问题标题】:Rails: Avoiding duplication errors in Factory Girl...am I doing it wrong?Rails:避免 Factory Girl 中的重复错误……我做错了吗?
【发布时间】:2011-08-16 00:05:38
【问题描述】:

假设我有一个模型user,它对email 字段有唯一性约束

如果我在一切正常后调用Factory(:user),但如果我再次调用它,它将失败并出现“条目已存在”错误。

我目前正在使用一个简单的帮助器在创建工厂之前搜索数据库中的现有条目......并调用我通过该帮助器创建的任何工厂。

它有效,但并不完全优雅,考虑到我认为这个问题一定很常见,我猜有更好的解决方案。那么,工厂女孩有没有一种内置的方式来return_or_create 工厂,而不是直接用create() 冲锋陷阵?如果没有,大多数人如何避免与他们的工厂重复条目?

【问题讨论】:

  • 我也遇到了这个问题。您是否在电子邮件字段中添加了一个序列,所以理论上每次调用 Factory(:user) 时它都会改变。我已经做好了,但仍然遇到您遇到的问题。
  • 我遇到了同样的问题。我注意到 FactoryGirl 在我的测试数据库中留下了一些来自早期测试的不良数据,该测试严重失败并且可能引发异常(可能避免清理)。我通过这样做来修复它:RAILS_ENV=test bin/rake db:drop RAILS_ENV=test bin/rake db:create RAILS_ENV=test bin/rake db:migrate 这清除了所有旧数据。希望这有助于@someoneinomaha

标签: ruby-on-rails-3 factory-bot


【解决方案1】:

简单答案:使用 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

注意您必须使用 Random.rand 而不是 rand() 因为 FactoryGirl 中的冲突(错误?)[https://github.com/thoughtbot/factory_girl/issues/219](see 这里)。

这使您可以从命令行随意创建用户,而不管数据库中是否已经存在工厂生成的用户。

使电子邮件测试更容易的可选附加功能

当您进行电子邮件测试时,您通常希望验证特定用户的操作是否触发了发送给其他用户的电子邮件。

您以Robin Hood 登录,发送电子邮件至Maid Marion,然后前往您的收件箱进行验证。您在收件箱中看到的内容来自user_842@factory.com。那到底是谁?

您需要返回您的数据库以检查电子邮件是否由您预期的任何人发送/接收。再一次,这有点痛苦。

我喜欢做的是使用工厂用户的名称和随机数生成电子邮件。这使得检查东西来自谁变得容易得多(并且也使碰撞几乎不可能消失)。使用 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

这为我们提供了风度翩翩但独特的电子邮件,例如 robin_hood_341@factory.commaid_marion_10@factory.com

【讨论】:

  • 但是,这样做的缺点是电子邮件地址可能与名称不同。我现在明白你在这里想要做什么。此外,ffaker 比经典的 Faker FWIW 更快、表现更好。
  • 然后使用Faker::Internet.email("#{first_name} #{last_name}") 使电子邮件与名称匹配。
  • 我只是想强调传递一个块而不是一个参数对于避免模型属性的懒惰(一次性)生成是必要的。例如,在user.email {"user_#{rand(1000).to_s}@factory.com" }; user.password{ "secret" } 中,{} 是必要的,以确保每次工厂构建用户时都会生成一个新的随机字符串。如果没有括号,相同的随机字符串将继续被重复使用。 OTOH密码属性周围不需要括号。
  • 使用 user.email {"user_#{rand(1000).to_s}@factory.com" } 会抛出错误:未定义的方法 `rand='。相反,使用:user.email {"user_#{Random.rand(1000)}@factory.com" }。另外,#{} 是字符串插值,所以例子中的 .to_s 是多余的。
  • @MarnenLaibow-Koser 这些天 ffaker 并不快。从他们的自述中:“从那时起,faker 也被重写了,“速度”因素现在可能已经无关紧要了。”
【解决方案2】:

以下是我强制工厂女孩序列中的“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

【讨论】:

    【解决方案3】:

    我发现这是确保测试始终通过的好方法。 否则你无法确定 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
    

    【讨论】:

      【解决方案4】:

      如果您只需要为属性生成几个值,您还可以向 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
      

      注意:这不应该用于很多属性,因为我们会跟踪所有先前的字符串(可能的优化)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-02-12
        • 1970-01-01
        • 2021-03-18
        • 1970-01-01
        • 2012-06-12
        • 1970-01-01
        • 2018-02-16
        相关资源
        最近更新 更多