Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make Ruby test factories with random unique data, in Factory Girl or Minifacture?

I am testing a typical Rails model with a typical factory:

# 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

I am adding more tests and starting to see collisions in the airport code: for example the factory creates an airport with code "XYZ" then a subsequent call to the factory tries to create an airport with the same code.

A sequence is one way to tackle this. For example use a Factory Girl sequence, or an ordered list, or pre-calculated enumeration,some similar way of maintaining state of the next available code.

My question is: what are non-sequence ways to tackle this? I want to use random data, and not a sequence.

A few ideas I'm trying because they're pragmatic -- any insight on these is much appreciated.

Example idea to use optimistic locking

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

Pros: fast in practice because collisions are rare; local state.

Cons: awkward syntax; non-local to the factory; the save might fail for reasons other than the collision.

Example idea to use a transaction

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

Pros: this is the closest to what I want; local state; ensures there's no collision.

Cons: long awkward syntax.

Bounty

Does Factory Girl or Minifacture have any kind of syntax that is more amenable to random data, and not a sequence?

Or perhaps some kind of pattern to automatically re-roll of the dice if there's a save collision?

Some overhead is fine with me. In practice a collision is happening once per day or so, on a continuous integration setup with thousands of tests. If the test suite must re-roll the dice a few times, or probe the database for existing values, etc. that's fine.

The comments ask why random data instead of a sequence. I prefer random data because my experience is that random data leads to better tests, better long-term maintainability, and better semantics with the test goal. Also, I use Faker and Forgery instead of fixtures, in case that's helpful to know.

To earn the bounty, the answer must be random on the fly-- not a sequence. (For example, the solution I'm seeking may likely use #sample and/or an unordered set, and may likely not use #shuffle and/or an ordered set)

like image 400
joelparkerhenderson Avatar asked Apr 29 '14 02:04

joelparkerhenderson


People also ask

What is Factory Girl in Ruby?

Factory Bot, originally known as Factory Girl, is a software library for the Ruby programming language that provides factory methods to create test fixtures for automated software testing.

What is Build_stubbed?

build_stubbed is the younger, more hip sibling to build ; it instantiates and assigns attributes just like build , but that's where the similarities end.

What is Factorybot in Rspec?

Factory Bot is a helper for writing factories for Ruby tests. It was previously known as Factory Girl.


2 Answers

You could use a callback. Something like:

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

You may want to change after(:build) to before(:create), it depends on how you want to use the factory.

like image 107
Gergo Erdosi Avatar answered Oct 10 '22 08:10

Gergo Erdosi


this should work, but it only allows for 17576 models to be created

CODES = ("AAA".."ZZZ").to_a.shuffle
Factory.define :airport do |f|
  f.code { CODES.pop }
end
like image 35
phoet Avatar answered Oct 10 '22 09:10

phoet