Looping sequences in Factory Girl.

390 views
Skip to first unread message

Vikrant Chaudhary

unread,
Feb 9, 2012, 6:39:38 AM2/9/12
to factor...@googlegroups.com
Hi,
I've a model Country with an attribute "code", which can only contain 2 characters (intended for ISO 3166-1 alpha-2 code).

In test/factories/countries.rb, I have

FactoryGirl.define do
  factory :country do
    sequence(:code, 'AA')
  end
end

However, this allows only a maximum of 26*26=676 Country records, after which validation starts failing (I've dozens of models and pretty much every model has association with Country). So I was wondering, if is it possible to loop over sequences in factory_girl? Or does this also count as an anti-pattern like resetting? - http://robots.thoughtbot.com/post/10983530140/getting-sequential-a-look-into-a-factory-girl

I wanted something like
 
  sequence(:code, 'AA'..'ZZ')

I think that this is a very general problem, however I couldn't find any references to it on web, which leads me to suspect that maybe I'm doing it wrong?

Vikrant Chaudhary

unread,
Feb 23, 2012, 3:24:06 AM2/23/12
to factor...@googlegroups.com
Ok, so this is what I'm doing now to loop 

#lib/loop_sequence.rb
class LoopSequence
  def initialize start, finish
    @cursor = @start = start
    @finish = finish
  end
  
  def to_s
    @cursor
  end
  
  def next
    #'AAA' is < 'ZZ', so can't do @cursor.next > @finish
    @cursor = (@cursor.next == @finish.next ? @start : @cursor.next)
    self
  end
end

#test/factories/countries.rb
FactoryGirl.define do
  factory :country do
    sequence :code, LoopSequence.new('AA', 'ZZ')
  end
end

Vikrant Chaudhary

unread,
Feb 23, 2012, 3:36:26 AM2/23/12
to factor...@googlegroups.com
I've just realised that in my above post @cursor.next == @finish.next doesn't really make sense. Correcting.

#lib/loop_sequence.rb
class LoopSequence
  def initialize start, finish
    @cursor = @start = start
    @finish = finish
  end
  
  def to_s
    @cursor
  end
  
  def next
    @cursor = @cursor == @finish ? @start : @cursor.next
    self
  end
end

Peter Vandenabeele

unread,
Feb 23, 2012, 4:30:41 AM2/23/12
to factor...@googlegroups.com
Hi Vikrant,

I see you are still working on this issue?

I won't say my solution is optimal, but what I typically do for such a 
standard case of "countries" etc. that are stored in tables is to make
one "default" country that is used for most tests as the default, and
only vary the country when the test is really about the country
attribute.

So that could be (untested code, written from top of my head)

FactoryGirl.define do

  # your original code
  factory :country do
    sequence(:code, 'AA')
  end

  factory :belgium, :parent => :country do
    code 'BE' # do not consume a sequence
  end

end

FactoryGirl.define do

  factory :address do
    ...
    # do not use "associate" because build hits the db
    country {FactoryGirl.build(:belgium)} 
  end

  factory :address_in_france, :parent => :address do
    ...
    country {FactoryGirl.build(:country, :code => 'FR')} 
    city "Paris"
  end

end

With this approach, all "default" address factories will just reuse
Belgium as country (avoiding the sequence overflow). When I
need a dedicated test for another country, I would then do something like:

it "country_code from country.code for France" do
  address = FactoryGirl.build(:address_in_france)
  address.country_code.should == "FR"
end

Of course this does not really address your original question of
sequencing in a set of limited size. But it is how I avoid this issue
altogether.

HTH,

Peter

--
*** Available for a new project ***

Peter Vandenabeele
http://twitter.com/peter_vhttp://coderwall.com/peter_v

Vikrant Chaudhary

unread,
Feb 23, 2012, 5:03:37 AM2/23/12
to factor...@googlegroups.com, pe...@vandenabeele.com
Thanks Peter.

So far, I've been doing somewhat exactly that.
So instead of 

country {FactoryGirl.build(:belgium)} 

I'd been writing

country { Country.first || FactoryGirl.create(:country) }

However, since I've come up with the code in the third post of this thread, it has been serving me well. Now it doesn't feel like hacky any more. :-)

Peter Vandenabeele

unread,
Feb 23, 2012, 5:35:14 AM2/23/12
to factor...@googlegroups.com
On Thu, Feb 23, 2012 at 11:03 AM, Vikrant Chaudhary <nas...@gmail.com> wrote:
Thanks Peter.

So far, I've been doing somewhat exactly that.
So instead of 

country {FactoryGirl.build(:belgium)} 

I'd been writing

country { Country.first || FactoryGirl.create(:country) }

A little off-topic as you do have a different cyclical LoopSequence
now, but a difference that I find relevant is that this code above
will touch the database every time (first trying to find a country
and second creating one if not present; which could be quite often
if the test framework correctly rolls back the transaction after every
test).

My proposed code explicitly does not touch the database
when there are no create/save steps in the test (which can
be avoided in most business logic tests, if one pays some
attention to it). I started to become sensitive to that, to try to
make my tests and factory designs less dependent on the
persistence layer.

Especially as you mentioned that a "country" will be present
in most factory objects you create, it could be relevant for
performance and for decoupling the tests from the
persistence layer in general ...

HTH,

Peter

Vikrant Chaudhary

unread,
Feb 24, 2012, 1:16:25 AM2/24/12
to factor...@googlegroups.com, pe...@vandenabeele.com
Yup! I was aware of the more database hits but that was all that I'd been able to come up with. Its all solved now though with LoopSequence. I guess this sort of functionality should already be in core. Something like 
Reply all
Reply to author
Forward
0 new messages