Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

An attributes_for method that builds associations and also returns their ID's #408

Closed
jmuheim opened this Issue · 5 comments

3 participants

@jmuheim

In a controller spec of a Rails 3 app, I'd like to have an easy way to populate the valid_attributes method with the help of FactoryGirl.

Sadly, attributes_for does not create needed associations, while (FactoryGirl.build :position).attributes.symbolize_keys returns too much stuff (e.g. timestamps which may not be available for mass assignment). So here's my solution:

(FactoryGirl.build :position).attributes.symbolize_keys.reject { |key, value| !Position.attr_accessible[:default].collect { |attribute| attribute.to_sym }.include?(key) }

It would be nice, though, to have a method built right into FG which provides this functionality.

@joshuaclayton

@sientia-jmu this is an interesting solution; I see a couple issues though, so please let me know what your thoughts are.

Firstly, it requires use of attr_accessible to whitelist all attributes that can be assigned per model. While I dig this concept (and use it all the time in my Rails apps) I'm not sure everyone's on board with it yet. Second, model.attributes doesn't return associations, just their FKs. I wanted to confirm this is the desired behavior.

I whipped up a custom strategy that mimics attributes_for but includes some other craziness. It's a really huge hack since I don't expose publicly a lot of what this is doing, but if this seems reasonable (and others can agree on it), what I can do is expose the ability to grab attributes set on the factory from the evaluation so it doesn't require hacking and will be stable through later versions. Here it goes:

class AttributesForWithForeignKeys
  def association(runner)
    runner.run(:create)
  end

  def result(evaluation)
    attribute_assigner = evaluation.instance_variable_get(:@attribute_assigner)
    attribute_names = attribute_assigner.send(:attribute_names_to_assign)

    attribute_names.each do |attr|
      attribute_names += FactoryGirl.aliases_for(attr)
    end

    evaluation.object.attributes.symbolize_keys.slice(*attribute_names)
  end
end

FactoryGirl.register_strategy(:attributes_for_with_foreign_keys, AttributesForWithForeignKeys)

This would allow for

FactoryGirl.attributes_for_with_foreign_keys(:comment)

Here's the spec I wrote (which is green) - it's a great example of what would be carried over in the resulting hash.

require 'spec_helper'

describe 'attributes_for_with_foreign_keys' do
  let(:attributes_for_with_foreign_keys) do
    define_class 'AttributesForWithForeignKeys' do
      def association(runner)
        runner.run(:create)
      end

      def result(evaluation)
        attribute_assigner = evaluation.instance_variable_get(:@attribute_assigner)
        attribute_names = attribute_assigner.send(:attribute_names_to_assign)

        attribute_names.each do |attr|
          attribute_names += FactoryGirl.aliases_for(attr)
        end

        evaluation.object.attributes.symbolize_keys.slice(*attribute_names)
      end
    end
  end

  before do
    define_model("Post", title: :string, body: :text)
    define_model("Comment", body: :text, post_id: :integer) do
      attr_accessible :body, :post
      belongs_to :post
    end

    FactoryGirl.define do
      factory :post do
        title 'Great post!'
        body 'This is the body of a post!'
      end

      factory :comment do
        post
        body 'This is the body of a comment!'
      end
    end

    FactoryGirl.register_strategy(:attributes_for_with_foreign_keys, attributes_for_with_foreign_keys)
  end

  it 'only returns attributes specific to the result' do
    FactoryGirl.attributes_for_with_foreign_keys(:post).keys.should =~ [:title, :body]
  end

  it 'handles associations' do
    FactoryGirl.attributes_for_with_foreign_keys(:comment).keys.should =~ [:post_id, :body]
  end
end

You can see that it doesn't include id, timestamps, etc. It creates associations but doesn't save the primary object, nor does it run any sort of build or create FG callbacks.

The downside to this is that it won't use attributes other than the ones declared in the factory itself. Not sure if this is a deal-breaker; let me know your thoughts!

@joshuaclayton

@sientia-jmu Closing for now - let me know how this goes!

@jmuheim

Didn't have time to investigate this. I'll probably come back to this later. Thanks anyways!

@adampope

Sorry for digging up an old issues, but I was wandering if anything like this has made its way into FG or was in the works?

I tried using the strategy @joshuaclayton posted above and it works great for normal associations but it didn't seem to work for associations where I've had to customise the factory name. I've also customised the :foreign_key setting of the relationship, not sure if that's what's tripping it up.

factory :compliance_task, :class => 'Compliance::Task' do
    name "MyString"
    compliance_asset
    description "MyText"
    association :frequency, factory: :compliance_task_frequency
  end

In this example, I get

 {:name=>"MyString", :description=>"MyText", :compliance_asset_id=>1}

which is missing a :compliance_task_frequency_id for the :frequency association.

@joshuaclayton

@adampope what I threw together was by no means an end-all-be-all solution, and because each project is unique, requirements often change so it's unlikely this will actually be included within factory_girl itself. In cases like this, I'd suggest using pry and doing some debugging to see where you can get. If you come up with a good pattern, please post it here and I could make some of the private interactions (instance_variable_get, send) available publicly somehow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.