factory_bot icon indicating copy to clipboard operation
factory_bot copied to clipboard

attributes_for(:factory, some_association: foo) returns nil for :some_association

Open fsateler opened this issue 5 years ago • 6 comments

I have searched the issue list for something similar, but I have not found the exact same issue.

The problem is that, given these two models:

factory :employee do
  # various attributes
end

factory :job do
  association :employee, strategy: :build
end

attributes_for ignores any explicit employee assignments:

attributes_for(:job, employee: some_other_employee)[:employee] # this is nil!

build and create do not suffer from this problem. Removing the association annotation from the second factory solves the problem, but then I can't have a default association for create.

The following spec fails on current master:

  it "allows specifying association values" do
    sub = attributes_for(:post, user: User.new)
    expect(sub[:user]).not_to eq nil
  end

Note that the following does work:

  it "allows overriding association keys" do
    sub = attributes_for(:post, user_id: 5)
    expect(sub[:user_id]).to eq 5
  end

fsateler avatar Apr 30 '19 00:04 fsateler

That is the expected behavior for attributes_for. It does not include attributes for associations. There has been some confusion about this in the past (see https://github.com/thoughtbot/factory_bot/issues/687, for example) so maybe we can improve the documentation around this.

composerinteralia avatar Apr 30 '19 13:04 composerinteralia

Hi @composerinteralia , thanks for your quick response.

I think this issue is different from #687 , #450, #408, #359 or other related issues. Those issues are about attributes_for not returning the attributes for the associations that would be built during build or create. I agree they should not be returned there.

The problem here is that attributes_for is explicitly ignoring parameters I'm passing it:

# Assume foo and bar are *not* attributes of Job
attributes_for(:job, foo: :bar, bar: :foobar) # returns { foo: :bar, bar: :foobar }
attributes_for(:job, foo: :bar, bar: :foobar, employee: some_employee) # returns { foo: :bar, bar: :foobar }

In other words, I don't want attributes_for to build attributes for all associations. I want it to stop discarding a parameter I'm passing it.

fsateler avatar Apr 30 '19 16:04 fsateler

It discards it because it is an association. The fact that the value is passed in as an override doesn't change how attributes_for behaves; it always uses the null strategy for associations: https://github.com/thoughtbot/factory_bot/blob/99ac02400fd56bd1872fc3ed84f81ea5e8f27737/lib/factory_bot/strategy/attributes_for.rb#L4-L6

Could you share your use case? Perhaps we can find a workaround. We can also discuss the current behavior more, but changing it would be non-trivial and I would want to make sure there was a common use case before exploring that.

composerinteralia avatar Apr 30 '19 21:04 composerinteralia

My use case is that I have some methods that create the models, and I'd like to test those:

# in Job
def self.create_with_metadata params, metadata
  transaction do
    job = create!(params)
    do_stuff_with_metadata(metadata)
  end
end

# in the controller
def create
  @employee = Employee.find(params[:employee_id])
  job = Job.create_with_metadata(job_params.merge(employee: employee), metadata)
  redirect_to job_path(job)
rescue ActiveRecord::RecordInvalid => e
  @job = e.record
  render 'edit'
end

Now, I'd like to test this create_with_metadata method. My current workaround is relatively simple, but a bit annoying:

test "it should work with end_date" do
  employee = build(:employee)
  job = Job.create_with_metadata(attributes_for(:job, end_date: Date.today).merge(employee: employee))
  assert_equal Date.today, job.end_date
end

The annoying part is that, as a user, it is not obvious that by declaring an association you render the attribute unsettable in the attributes_for call. So every time I bump into this I waste a few minutes wondering why attributes_for(:job, employee: employee) doesn't work as I intended.

fsateler avatar May 02 '19 13:05 fsateler

We can also discuss the current behavior more, but changing it would be non-trivial

I can understand that, and maybe it is not worth fixing. However, I think the current behavior is not intuitive. Perhaps attributes_for should simply refuse (ie, raise an error) attributes it will clear later.

fsateler avatar May 02 '19 13:05 fsateler

Yeah, I see your point. Thanks for that example.

composerinteralia avatar May 17 '19 18:05 composerinteralia