thoughtbot / factory_bot

A library for setting up Ruby objects as test data.

Home Page:https://thoughtbot.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Generating nested attributes

rjasper-frohraum opened this issue · comments

Hi, I've been looking for a nice way to generate nested attributes with FactoryBot. There are already quite a few posts about this but I didn't find a solution which worked for me.

What I'd like to have is something like attributes_for which also resolves associations as hashes recursively. Below is a solution I came up with. (I am not sure whether this is the right place for this but maybe it is of some help.)

Register this strategy

class NestedAttributesForStrategy
  Config = Struct.new(:associations, :nested_attributes)

  def association(runner)
    runner.run(:nested_attributes_for)
  end

  def result(evaluation)
    config = Config.new([], {})

    evaluation.notify(:nested_attributes_for, config)

    result = evaluation.hash.merge(config.nested_attributes)

    config.associations.each do |association|
      result.delete(association)
    end

    result
  end
end

FactoryBot.register_strategy(:nested_attributes_for, NestedAttributesForStrategy)

Define global trait

FactoryBot.define do
  trait :with_nested_attributes do
    transient do
      nested_attributes_for { [] }
    end

    callback(:nested_attributes_for) do |config, ctx|
      associations = ctx.nested_attributes_for
      nested_attributes = config.nested_attributes

      config.associations = associations

      associations.each do |association|
        nested_attributes[:"#{association}_attributes"] = ctx.send(association)
      end
    end
  end
end

Use it in your factories

FactoryBot.define do
  # The models below normally wouldn't make for a good example of nested attributes.
  # Ignore OpenStruct. I used it so you don't need corresponding models for copy-paste testing.

  factory :post, class: 'OpenStruct' do
    with_nested_attributes

    transient do
      nested_attributes_for { %i[comments author] }
    end

    author
    comments { [association(:comment)] }
    content { 'Nice weather today!' }
  end

  factory :comment, class: 'OpenStruct' do
    with_nested_attributes

    transient do
      nested_attributes_for { %i[author] }
    end

    association :author, name: 'Jon Doe'
    message { 'I like your post' }
  end

  factory :author, class: 'OpenStruct' do
    name { 'Jane Doe' }
  end
end

Invoke

nested_attributes_for(:post).to_yaml

Result:

---
:content: Nice weather today!
:comments_attributes:
- :message: I like your post
  :author_attributes:
    :name: Jon Doe
:author_attributes:
  :name: Jane Doe

Good idea! Maybe it would be better to create a pull request?