Class: RuboCop::Cop::FactoryBotGuide::DynamicAttributeEvaluation

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector
Defined in:
lib/rubocop/cop/factory_bot_guide/dynamic_attribute_evaluation.rb

Overview

Checks that method calls in FactoryBot attribute definitions are wrapped in blocks for dynamic evaluation.

Without blocks, method calls are evaluated once at factory definition time, not at factory instantiation time. This causes all factory instances to share the same value, leading to subtle bugs and test pollution.

This is particularly problematic for:

  • Time-related methods (Time.now, Date.today, 1.day.from_now, etc.)
  • Random values (SecureRandom.hex, SecureRandom.uuid, etc.)
  • Object constructors (Array.new, Hash.new, etc.)
  • Any method calls that should return different values per instance

Examples:

Time-related methods

# bad - all users get the same timestamp
factory :user do
  created_at Time.now
  updated_at DateTime.now
  birth_date Date.today
  expires_at 1.day.from_now
end

# good - each user gets a fresh timestamp
factory :user do
  created_at { Time.now }
  updated_at { DateTime.now }
  birth_date { Date.today }
  expires_at { 1.day.from_now }
end

Random values

# bad - all users share the same token
factory :user do
  token SecureRandom.hex
  uuid SecureRandom.uuid
end

# good - each user gets a unique token
factory :user do
  token { SecureRandom.hex }
  uuid { SecureRandom.uuid }
end

Object constructors

# bad - all users share the same array/hash instance!
factory :user do
  tags Array.new
   Hash.new
end

# This causes test pollution:
user1 = create(:user)
user1.tags << 'admin'
user2 = create(:user)
user2.tags # => ['admin'] - unexpected!

# good - each user gets a new array/hash
factory :user do
  tags { Array.new }
   { Hash.new }
end

Static values (no block needed)

# good - static values don't need blocks
factory :user do
  name "John Doe"
  age 30
  active true
end

Complex expressions

# bad - method chains evaluated once
factory :user do
  full_name current_user.profile.display_name
end

# good - method chains evaluated per instance
factory :user do
  full_name { current_user.profile.display_name }
end

Direct Known Subclasses

DynamicAttributesForTimeAndRandom

Constant Summary collapse

MSG =
"Use block syntax for attribute `%<attribute>s` because `%<method>s` " \
"is evaluated once at factory definition time. " \
"Wrap in block: `%<attribute>s { %<value>s }`"
TIME_CLASSES =
%w[Time Date DateTime].freeze
RANDOM_CLASSES =
%w[SecureRandom].freeze

Instance Method Summary collapse

Instance Method Details

#attribute_assignment?(node) ⇒ Object



109
110
111
# File 'lib/rubocop/cop/factory_bot_guide/dynamic_attribute_evaluation.rb', line 109

def_node_matcher :attribute_assignment?, "(send nil? $_ $_value)\n"

#factory_block?(node) ⇒ Object



102
103
104
105
106
# File 'lib/rubocop/cop/factory_bot_guide/dynamic_attribute_evaluation.rb', line 102

def_node_matcher :factory_block?, "(block\n  (send {nil? (const {nil? cbase} :FactoryBot)} :factory ...)\n  ...)\n"

#on_block(node) ⇒ Object



113
114
115
116
117
118
119
120
# File 'lib/rubocop/cop/factory_bot_guide/dynamic_attribute_evaluation.rb', line 113

def on_block(node)
  return unless factory_block?(node)

  # Check all attribute assignments within the factory
  node.each_descendant(:send) do |send_node|
    check_attribute(send_node)
  end
end