≡

wincent.dev

  • Products
  • Blog
  • Wiki
  • Issues
You are viewing an historical archive of past issues. Please report new issues to the appropriate project issue tracker on GitHub.
Home » Issues » Feature request #1283

Feature request #1283: Handle "circular dependencies" in FixtureReplacement?

Kind feature request
Product wincent.dev
When Created 2009-04-21T05:35:52Z, updated 2010-06-24T09:04:04Z
Status closed
Reporter Greg Hurrell
Tags no tags

Description

Given two models:

class User < ActiveRecord::Base
  has_many                  :emails, :dependent => :destroy
end

class Email < ActiveRecord::Base
  belongs_to              :user
end

And corresponding FixtureReplacement db/example_data.rb:

module FixtureReplacement
  def email_attributes
    {
      :address                  => "#{String.random}@example.com",
      :user                     => default_user,
      :verified                 => true
    }
  end

  PASSPHRASE = 'supersecret'
  def user_attributes
    {
      :display_name             => String.random,
      :passphrase               => PASSPHRASE,
      :passphrase_confirmation  => PASSPHRASE,
      :verified                 => true
    }
  end
end

We have a classic "circular" dependency problem.

create_user will give us a "valid" User object (which passes all validations) but which isn't totally "ready to go" in practice, because it doesn't have any associated email with it.

On the other hand, create_email will automatically set up an associated User instance for us. This is because in order to be valid it needs an associated User instance; I haven't set up a Rails-level validation for this, but at the database level there is indeed a non-NULL constraint:

  create_table "emails", :force => true do |t|
    t.integer  "user_id",                       :null => false
    t.string   "address",    :default => "",    :null => false
    t.boolean  "default",    :default => true,  :null => false
    t.boolean  "verified",   :default => false, :null => false
    t.datetime "created_at"
    t.datetime "updated_at"
    t.datetime "deleted_at"
  end

In other words, you can't save an email record to the database without an associated user record. (Is that the right thing to do?)

So in our specs if you want to test the Email model you just do create_email and you're ready to go. If you want to test the User model you can often just do create_user but if you want to get a model which really is "ready to go" (with an associated, verified email) then you must do something like this:

email = create_email
user = email.user

This lack of symmetry is a little bit annoying; why can't FixtureReplacement give you a reasonable, ready-to-go default object for both classes? If you try to set up associated email records in your db/example_data.rb file then you'll create a circular dependency (to create a user instance you need an email, but to create an email you need a user).

So there are two or three possible solutions here:

  • add another helper method to really give you a ready-to-go user instance: this is basically a wrapper for the email = create_email; user = email.user pattern.
  • patch FixtureReplacement so that it will accept some kind of block or Proc parameter in the example data in which you can do arbitrary set-up or overriding; in this block or Proc you'd evidently call create_email whenever asked for a new user, and then hand back the user.
  • rethink my object dependency graph: perhaps I am doing the wrong thing by requiring an existing user object before saving an email record.

Comments

  1. Greg Hurrell 2009-04-21T05:42:29Z

    Status changed:

    • From: New
    • To: Open
  2. Greg Hurrell 2009-11-26T16:43:33Z

    FixtureReplacement 3.0 handles this, as noted here, so looks like the problem might go away.

  3. Greg Hurrell 2010-06-24T09:02:34Z

    Factory Girl also handles it without a problem:

    Factory.define :email do |e|
      e.address { Sham.email_address }
      e.association :user
      e.verified true
    end
    
    Factory.define :user do |u|
      u.display_name { Sham.user_display_name }
      u.passphrase { Sham.passphrase }
      u.passphrase_confirmation { Sham.passphrase }
      u.verified true
    
      # an associated email is not "required" (for validation) but it
      # is still necessary in practice if the record is to be usable
      u.after_create { |user| Email.make! :user => user  }
    end

    Calling Email.make/Email.make! always creates the associated user object.

    And as noted in the comment User.make won't try to make an associated email, but User.make! will set one up via the after_create callback.

    Works very nicely, so marking as closed.

  4. Greg Hurrell 2010-06-24T09:04:04Z

    Status changed:

    • From: open
    • To: closed
Add a comment

Comments are now closed for this issue.

  • contact
  • legal

Menu

  • Blog
  • Wiki
  • Issues
  • Snippets