thoughtbot / thoughtbot/factory_bot

Generating nested attributes

Open
#1,545 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
8.2k
Forks
2.6k
PR merge metrics
No merged PRs in 30d

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by comparing the proposed nested_attributes_for behavior with the existing attributes_for entry point and register_strategy API. Reproduce the post/comment/author example to assess recursive association hashes and *_attributes keys. No files or tests are named; done means a supported, tested approach for generating the requested nested hashes.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.