thoughtbot / factory_bot

A library for setting up Ruby objects as test data.
https://thoughtbot.com
MIT License
7.91k stars 2.6k forks source link

Associations blocks are being executed with invalid data at attributes_for #1589

Open cesarjr opened 1 year ago

cesarjr commented 1 year ago

Description

Hi team!

I noticed that FactoryBot is executing the associations block even when I'm using attributes_for.

When I have one block which depends on another association, ruby is going to generate an undefined method "the-association-name" for nil:NilClass.

I expected that the associations blocks weren't executed.

Thanks. You're great 💜!

Reproduction Steps

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"
  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
  gem "factory_bot", "~> 6.0"
  gem "activerecord"
  gem "sqlite3"
end

require "active_record"
require "factory_bot"
require "minitest/autorun"
require "logger"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :blogs, force: true do |t|
    t.string :name
  end

  create_table :authors, force: true do |t|
    t.string :name
    t.references :blog
  end

  create_table :posts, force: true do |t|
    t.string :body
    t.references :blog
    t.references :author
  end
end

class Blog < ActiveRecord::Base
  has_many :authors
  has_many :blogs
end

class Author < ActiveRecord::Base
  belongs_to :blog
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :blog
  belongs_to :author
end

FactoryBot.define do
  factory :blog do
    name { "the blog name" }
  end

  factory :author do
    name { "the author name" }
  end

  factory :post do
    body { "the post body" }
    blog

    # This block should not be executed
    # on FactoryBot.attributes_for(:comment)
    author { blog.authors.first }
  end
end

class FactoryBotTest < Minitest::Test
  def test_factory_bot_stuff
    post = FactoryBot.create(:post)
    assert_equal post.author, post.blog.authors.first
    # true

    FactoryBot.attributes_for(:post)
    # NoMethodError: undefined method `authors' for nil:NilClass
  end
end

Expected behavior

I expected that the associations blocks weren't executed or the associations data were valids.

Actual behavior

The associations blocks are being executed with invalid data.

ellnix commented 11 months ago

Unfortunately, giving a block to author means it will be recognized as a dynamic attribute, not an association (internally therefore there will be no distinction between body and author, they are simply fields FactoryBot populates by running a block).

In addition, there is a deep rooted assumption in the code base that an association cannot be passed a block, it can only have traits and attribute overrides.

Maybe this post factory could work for you?

  factory :post do
    body { "the post body" }
    author
    blog { association :blog, authors: [author] }
  end
ezekg commented 9 months ago

Do you think it'd make sense for factory_bot to infer associations where possible?