【问题标题】:How to make Ruby test factories with random unique data, in Factory Girl or Minifacture?如何在 Factory Girl 或 Minifacture 中使用随机唯一数据制作 Ruby 测试工厂?
【发布时间】:2014-04-29 02:12:03
【问题描述】:

我正在用一个典型的工厂测试一个典型的 Rails 模型:

# My model uses a 3-letter uppercase airport code,
# such as "ATL" for Atlanta, "BOS" for Boston, etc.

class Airport < ActiveRecord::Base
  validates :code, uniqueness: true

Factory.define :airport do |f|
  f.code { random_airport_code }  # Get a 3-letter uppercase code

我正在添加更多测试并开始在机场代码中看到冲突:例如,工厂使用代码“XYZ”创建了一个机场,然后对工厂的后续调用尝试使用相同的代码创建一个机场。

序列是解决此问题的一种方法。例如,使用 Factory Girl 序列、有序列表或预先计算的枚举,一些类似的方式来维护下一个可用代码的状态。

我的问题是:解决这个问题的非序列方法是什么?我想使用随机数据,并且不是一个序列。

我正在尝试一些想法,因为它们很实用 - 非常感谢您对这些想法的任何见解。

使用乐观锁的示例思路

while 
  airport = Factory.build :airport
  airport.save && return airport
end

优点:在实践中速度很快,因为很少发生碰撞;地方州。

缺点:语法笨拙;非本地工厂;保存可能由于碰撞以外的原因而失败。

使用事务的示例想法

Airport.transaction 
  while
    x = random_airport_code
    if Airport.exists?(code: x)
      next
    else
      Factory :airport, code: x
      break
    end
  end
end

优点:这是最接近我想要的;当地国家;确保没有碰撞。

缺点:冗长笨拙的语法。

赏金

Factory Girl 或 Minifacture 是否有更适合随机数据且不是序列的语法?

或者,如果有保存冲突,也许某种模式可以自动重新掷骰子?

一些开销对我来说很好。实际上,在具有数千个测试的持续集成设置中,每天大约会发生一次冲突。如果测试套件必须重新掷骰子几次,或者探测数据库中的现有值等,那很好。

cmets 询问为什么随机数据而不是序列。我更喜欢随机数据,因为我的经验是随机数据可以带来更好的测试、更好的长期可维护性以及更好的测试目标语义。另外,我使用 Faker 和 Forgery 而不是固定装置,以防万一了解。

要获得赏金,答案必须是即时随机的,而不是顺序。 (例如,我正在寻找的解决方案可能使用#sample 和/或无序集,并且可能不使用#shuffle 和/或有序集)

【问题讨论】:

  • 随机数据相对于序列有什么优势?测试中的随机数据可能会导致间歇性故障,并且难以诊断或重现。
  • 也想知道随机的原因。对于序列,您可以 100% 确信您可以拥有 AAA 到 ZZZ 的唯一数据而不会发生冲突。但是对于随机,您要么创建一个不会发生冲突的随机生成器(这可能还是容易?),或者检查生成的数据(性能缓慢)
  • 也许你想看看 Faker (github.com/stympy/faker) - 你可以使用 Faker::Lorem.characters(3) 并将它们转换为大写字母进行测试。
  • 感谢 George & Peter & George;我添加了更多细节,是的,我正在使用 Faker。
  • 我不知道你想要达到什么目的。如果你想要“随机”数据,faker 应该足够好。如果您不希望发生冲突,请使用序列,那应该就足够了。在最坏的情况下,您可以将 faker 和序列结合起来以始终生成“随机”数据。

标签: ruby-on-rails ruby rspec factory-bot minitest


【解决方案1】:

您可以使用回调。比如:

factory :airport do
  after(:build) do |airport|
    airport.code = loop do
      code = ('AAA'..'ZZZ').to_a.sample
      break code unless Airport.exists?(code: code)
    end
  end
end

您可能想将after(:build) 更改为before(:create),这取决于您要如何使用工厂。

【讨论】:

  • 感谢 Gergo,您的回答最接近我真正随机的目标,在发生碰撞的极少数情况下重试。非常感谢!
【解决方案2】:

这应该可以,但它只允许创建 17576 个模型

CODES = ("AAA".."ZZZ").to_a.shuffle
Factory.define :airport do |f|
  f.code { CODES.pop }
end

【讨论】:

  • 感谢 phoet。我正在寻找一种不使用序列的解决方案,换句话说,不需要非本地状态的 CODES 数组。
  • 会员够本地吗? { (@codes ||= ("AAA".."ZZZ").to_a.shuffle).pop }我真的很喜欢你如何解决这样一个微不足道的问题
  • 您评论中的成员仍将存储序列,即知道每个下一个元素并随时间保持其状态的有序集。我的目标是每次都使用随机选择,而不是顺序。
  • 在从 AAA 到 ZZZ 的可能值的极限空间中,几乎没有可用的随机性。你想如何在没有开销的情况下实现碰撞检测?
  • 开销对我来说很好——我将把它添加到问题陈述中。每个测试都会清除数据库,并且每个测试都需要几个机场才能正确运行,因此实际值只是可能值空间的一小部分,即我正在选择 17576 中的 5 个。
【解决方案3】:

是的,FactoryGirl 有一个功能可以让你做到这一点。请参阅序列文档的结尾:https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#sequences 您可以将序列设置为任何知道如何在调用 #next 时返回自身递增版本的对象。因此,您可以编写一个知道如何返回唯一随机数据并实现#next 的类,例如

class AirportCode
  ALL = %w(AAA BBB CCC).shuffle

  attr_reader :index

  def initialize(index = rand(ALL.length))
    @index = index
  end

  def value
    ALL[@index]
  end

  def to_s
    value
  end

  # might need to explicitly delegate more methods to the value

  def method_missing(method, *args)
    value.send method, *args
  end

  def next
    AirportCode.new((index + 1) % ALL.length)
  end

end

(这个只有三个唯一值,但只是为了说明这一点),创建一个 FactoryGirl 序列并将其值设置为该类的一个实例。我没有尝试过 FactoryGirl 部分,所以请报告它是否有效 :)

【讨论】:

  • 谢谢戴夫。我正在寻找不使用序列的解决方案。
  • 仍然不清楚为什么 factory_girl 的序列构造不应该成为解决方案的一部分。我知道您不想要顺序数据,但是如果您可以像上面那样欺骗 factory_girl 序列返回非顺序数据,为什么不呢?
  • 因为您的答案中的序列仍然是一个序列,即它仍然需要维护列表顺序并跟踪索引。这与问题中的“预先计算序列”示例基本相同。我正在寻求的是一种无需跟踪代码状态的解决方案。 (例如,理想的解决方案适用于分布式设置,工厂在不同的机器上运行,都与同一个数据库通信)
【解决方案4】:

与@GergoErdosi 的回答类似,我能够做到这一点:

CODES = ("AAA".."ZZZ").to_a.shuffle

factory :airport do after(:build) do |airport| if Airport.exists?(code: airport.code) new_code = ('AAA'..'ZZZ').to_a.sample airport.code = new_code end end code { CODES.rotate!.first } ... #other stuff for building Airports end

【讨论】:

  • 感谢您的回复。我相信您答案的核心价值是 GergoErdosi 的 after(:build) 重试和我的示例中的 #rotate 的混合体。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-01-27
  • 2014-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多