thoughtbot / thoughtbot/factory_bot

How to handle polymorphic associations using exclusive arc

Open
#1,489 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Situation

Currently, modeling polymorphic associations is pretty simple, as demonstrated in GETTING_STARTED.md. However, I have a polymorphic association using the Exclusive Belongs To/Exclusive Arc method (also seen here using Rails), which makes the above solution not work anymore because I have to make two optional associations instead of a single polymorphic one (I've left out the validation logic for clarity):

class Team < ActiveRecord::Base
  has_many :posts
end

class User < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :team, optional: true
  belongs_to :user, optional: true
end

This leads me to have to awkwardly define the associations in an after(:build) block, relying on a transient and traits that only set the transient to determine which of the two associated models to build/create:

FactoryBot.define do
  factory :team
  factory :user

  factory :post do
    transient do
      # default to :team
      team_or_user { :team }
    end

    trait :team_owned do
      team_or_user { :team }
    end

    trait :user_owned do
      team_or_user { :user }
    end

    after(:build) do |post, evaluator|
      if evaluator.team_or_user == :team
        post.team = evaluator.team || evaluator.association(:team, strategy: :build)
      elsif evaluator.team_or_user == :user
        post.user = evaluator.user || evaluator.association(:user, strategy: :build)
      end
    end
  end
end

As seen above, I call strategy: :build, but that's not ideal because I don't know the chosen strategy (which could lead to issues or missing data in other strategies). I also have to rely on the consumers of this factory to know to pass in team or user without being able to see it in the main factory body (defined like a regular association). I feel like there are other edge cases I'm not considering yet, but I haven't come across them yet. Finally, this is a lot of work to get a "simple" association to work properly.

Question/Discussion

Is this the best solution? Any ideas for better ways to do this that aren't so cumbersome? It makes me wish for an "anti-trait", a way to say "in this invocation, ignore the specified trait" so I could just put the associations in their traits and then put the anti-traits in the opposite traits, making it "obvious":

  factory :post do
    team_owned
    trait :team_owned do
      anti_trait :user_owned
      team
    end

    trait :user_owned do
      anti_trait :team_owned
      user
    end
  end

REPRODUCTION_SCRIPT.txt

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 with REPRODUCTION_SCRIPT.txt and the polymorphic-associations section of GETTING_STARTED.md, then compare them with the factory definition shown in the issue. The issue has no failing test or defined implementation target; a contribution would first need to establish the desired behavior for exclusive-arc associations and strategies.

Written by the indexing model from the issue text.

Assessment

Tech stack
rails, ruby
Domain
testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.