asemanfar - a blog about programming

Factory Girl with Cross-Validating Attributes

November 06, 2008

Just as a little background, I'm working on adding the concept of rounds to an application. A "round" is simply an organization unit with a start and end date. Each round has_many :events, and each event has an end date, defined as its moment of expiration. The validation logic for these two models is that an event's end date must be between the round's start/end dates and that rounds cannot overlap. This isn't an issue at all with things like fixtures since that's all manually specified. But I'm using FactoryGirl for generating my test data so things get a little tricky.

The problem I ran into is that an event requires a round with valid start/end dates, but the round factory generates its own sequential non-overlapping rounds. There are a few requirements I'm looking for:

  1. Independently generate rounds that do not overlap.
  2. Generate an event given a round, and pick a valid event end date.
  3. Generate an event given an end date, and generate a valid round.

This is what I came up with to do this:

   1  # Round Factory
   2  Factory.sequence :round_start_date do |n|
   3    ((n - 1) * 14).days.from_now
   4  end
   5  
   6  Factory.define :round do |round|
   7    round.starts_at { Factory.next(:round_start_date) }
   8    round.ends_at { |r| r.starts_at + 14.days }
   9  end
  10  
  11  # Event Factory
  12  Factory.define :event do |event|
  13    event.round do |e|
  14      if e.event_ends_at
  15        e.association :round, 
  16              :starts_at => e.event_ends_at - 10.days, 
  17              :ends_at => e.event_ends_at + 1.day
  18      else
  19        e.association :round
  20      end
  21    end
  22    event.event_ends_at { |e| e.round.ends_at - 2.days }
  23  end

So the round factory is pretty simple. The round_start_date sequence just insures that rounds generated using that factory do not overlap. This meets the 1st requirement.

To satisfy the 2nd and 3rd requirement, in the event factory, I first check to see if an end date for the event was specified in the factory build options, in which case I pass valid attributes to the round factory. Otherwise, I delegate the start/end date selection to the round factory. I then specify the event end date based on the round end date, relying on the fact that the event end date won't be set if it was specified using the build options hash. Phew! That's a mouth full.

Although this is quite ugly, it seems to work well with one caveat: I'm not guaranteed to have non-overlapping rounds created by the event factory when specified an event end date:

   1  Factory(:event, :event_ends_at => 5.days.ago)
   2  Factory(:event, :event_ends_at => 5.days.ago)

This will throw an exception because the second factory call will try to build a round with the same start/end date as the round created by the first. I couldn't come up with an elegant solution for that, the only thing I can think of is querying the database before creating a round and using a pre-existing event with proper date boundaries, but I don't want to do that; this works for me for now since my tests happen not to hit that caveat.

Does anyone have any similar experience or maybe a better solution?

Comments


Leave a Comment