How do I add extra data to join table in factory_bot factories
Asked Answered
U

3

6

I have models for Users and Sites with a has_many through relaitonship between them. There is one extra piece of data on the sites_users join table is_default which is type boolean with the intention being to allow each user to have one default site from the list of related sites for that user.

user model

class User < ApplicationRecord
  has_many :sites_users
  has_many :sites, through: :sites_users
  accepts_nested_attributes_for :sites_users, allow_destroy: true
  ...
end

user factory

factory :user do
  sequence(:email) { |n| "user_#{n}@example.com" }
  role { Role.find_by(title: 'Marketing') }
  image { Rack::Test::UploadedFile.new(Rails.root.join('spec', 'support', 'fixtures', 'user.jpg'), 'image/jpeg') }

  factory :super_admin do
    role { Role.find_by(title: "Super Admin") }
    admin true
  end

  before :create do |u|
    u.sites_users.build(site: site, is_default: true)
  end

end

alternate user factory Approach

On the User factory I have also tried this method included below, but cannot find a way to include the is_default: true using this syntax. So I ended up abandoning this method in favor of the above before_create call.

factory :user do
  ...
  site { site }
  ...
end

I would really appreciate any help anyone could provide. Thank you!

schema info

table: users

t.string "email", default: "", null: false
t.boolean "admin", default: false
t.integer "role_id"
t.string "first_name"
t.string "last_name"

table: sites

t.string "domain", default: "", null: false
t.string "name", default: "", null: false
t.string "logo"
t.string "logo_mark"

table: sites_users

t.bigint "site_id", null: false
t.bigint "user_id", null: false
t.boolean "is_default", default: false
Upwind answered 22/9, 2018 at 13:57 Comment(1)
Can you also include your sites and sites_users factory in the post above? Have you also tried to use after create instead of before create?Kessel
B
4

Create a factory for :site_user

factory :site_user, class: SiteUser do 
  site { site } # you could delete this line and add the site in factory :user
  is_default { false } # as specified in your DB
end

Instead of creating the site within the :user factory, create its relation using the nice syntax:

factory :user do 
  ...
  sites_users { [FactoryBot.build(:site_user, is_default: true)] }
  ...
end

It should do the trick!

Burette answered 24/10, 2018 at 19:10 Comment(1)
This works, I just needed to replace site { site } with site. Original throws: stack too deep error. THANK YOU!Upwind
B
0

So when I deal with having extra fields on my joins tables I create custom methods to build those join relations rather than trying to rely on rails built in methods. I have tested this method and it works with factory_bot just fine.


User Model

class User < ApplicationRecord
  ...

  def set_site(site, default = false)
    SiteUser.find_or_create_by(
      user_id: id,
      site_id: site.id
    ).update_attribute(:is_default, default)
  end

end

Note: This code is a block that I use so I would be able to both create new site relations and update the default value via the same method. You could simplify it to just create without checking for existence if you desired.

User Factory

factory :user do
  ...

  # to relate to new site with default true
  after(:create) do |u|
    u.set_site(create(:site), true)
  end

  # to relate to existing site with default true
  after(:create) do |u|
    u.set_site(site_name, true)
  end
end

Please let me know if this helps! (or if anyone has a more default railsish way that works just as well, I'd love to hear about it!)

Bacterin answered 24/10, 2018 at 15:38 Comment(0)
V
0

You might want to consider addressing this with a schema change. You could add a default_site_id column to the users table and manage the default site as a separate association of the user model.

In a migration:

add_foreign_key :users, :sites, column: :default_site_id

In User class:

class User < ApplicationRecord
  ...
  belongs_to :default_site, class_name: 'Site'
  ...

  # validate that the default site has an association to this user
  validate :default_site_id, inclusion: {in: sites.map(&:id)}, if: Proc.new {default_site_id.present?}
end

This will simplify the association and guarantee no user will ever have multiple site_users records where is_default is true. Setting the default site in a factory should be trivial.

Villar answered 25/10, 2018 at 3:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.