DEV Community

Nick Bell for LoadUp

Posted on

FactoryBot sidecar associations

The problem

Often, we create "sidecar" models alongside "primary" models in Rails apps. For instance, if you're developing a blogging platform, you may want to create the user's blog model when the user signs up. An example set of models:

class User
  has_one :blog
end

class Blog
  belongs_to :user
end

class UserCreationService
  def self.call(user_attrs:)
    user = User.create(user_attrs)
    Blog.create(user: user)
  end
end
Enter fullscreen mode Exit fullscreen mode

This works great in production, since we always run that service when users are created. However, when testing we might not want to use the service in every spec that requires a user. This is where a fixture/factory setup comes in.

Now, when testing your app in RSpec, I've found the ease of building models with FactoryBot to be a much better development experience than using Rails' standard fixtures. To build a factory for these, you might do something like this to start:

FactoryBot.define do
  factory :user do
    blog
  end

  factory :blog do
    user
  end
end
Enter fullscreen mode Exit fullscreen mode

Then, in your model specs:

describe User do
  subject { build(:user) }
  it { is_expected.to be_valid } # i.e. have a valid factory
end

describe Blog do
  subject { build(:blog) }
  it { is_expected.to be_valid }
end
Enter fullscreen mode Exit fullscreen mode

However, running these specs will give you an error like this:

SystemStackError:
  stack level too deep
Enter fullscreen mode Exit fullscreen mode

This happens because FactoryBot doesn't know that, when calling build(:user), (which calls build(:blog)), that the user is already present to be attached. Therefore build(:blog) will just call build(:user), and so on, creating the infinite loop.

The Solution

To fix this, we need to specifically assign the sidecar model to the primary, and vice versa, in the factories:

FactoryBot.define do
  factory :user do
    blog { association :blog, user: instance }
  end

  factory :blog do
    user { association :user, blog: instance }
  end
end
Enter fullscreen mode Exit fullscreen mode

This will properly associate the 1-1 models whether you call build(:user) or build(:blog). You can even still use an override attribute to create a model with override attributes.

2 examples, 0 failures
Enter fullscreen mode Exit fullscreen mode

Hopefully this helps. This should be one of many tiny tidbits.

Top comments (0)