thoughtbot / factory_bot

A library for setting up Ruby objects as test data.

Home Page:https://thoughtbot.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Associations blocks are being executed with invalid data at attributes_for

cesarjr opened this issue · comments

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.

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

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