ruby-on-rails NoMethodError 'each' on FactoryBot has_many through association

8fq7wneg  于 2023-03-31  发布在  Ruby
关注(0)|答案(2)|浏览(174)

我有一个工厂,它在引擎中生成一个设计用户w/角色。用户模型has_many :roles through: :roles_users。我可以让代码使用after(:create)子句,但不使用association:关键字。
这是可行的:
app/model/myengine/role.rb

module MyEngine
    class User < ActiveRecord::Base
        has_many :roles_users
        has_many :roles, through: :roles_users
    end
end

spec/factories/roles.rb

factory :role, class: "MyEngine::Role" do
    type: { 'admin' }
end
factory :user, class: "MyEngine::User" do
  sequence(:email) { |n| "tester_#{n}@example.com" }
  password { 'xxx' }
  password_confirmation { 'xxx' }      

  after(:create) do |user|
    user.roles << FactoryBot.create(:role)
  end 
end

但这不会,并且在初始化时undefined method 'each' for #<MyEngine::Role:0x0...>测试失败:

factory :user, class: "MyEngine::User" do
  sequence(:email) { |n| "tester_#{n}@example.com" }
  password { 'xxx'}
  password_confirmation { 'xxx' }      
  association: :roles, factory: :role
end

更新/编辑如下:

FactoryBot文档只是出于某种原因建议使用after(:create)钩子。从用户评论来看,上面的代码有两个问题:

  • 不使用集合
  • 附加关联时对象不存在

使用@Vasfed的建议,可以直接使用集合而不是对象来分配角色关联:

factory :user, class: "MyEngine::User" do
  sequence(:email) { |n| "tester_#{n}@example.com" }
  password { 'xxx'}
  password_confirmation { 'xxx' }      
  roles { [ create(:role) ] }
end

根据@ulferts的建议,使用new而不是create:

factory :user, class: "MyEngine::User" do
  sequence(:email) { |n| "tester_#{n}@example.com" }
  password { 'xxx'}
  password_confirmation { 'xxx' }      
  roles { [ build(:role) ] }
end

两者将产生:

ActiveRecord::RecordInvalid: Validation failed: Roles users is invalid

由于模型没有验证,这似乎指向FK表中缺少记录的问题或无法找到FK表,可能是由于名称空间解析。

ktecyv1j

ktecyv1j1#

该错误是因为您将角色上的单个示例传递给roles而不是集合。FactoryBot无法知道您想要创建多少个角色进行关联,因此无论如何都需要手动创建它们。
最简单的不带后置钩子的解决方法是roles { [ create(:role) ] }

efzxgjgh

efzxgjgh2#

我最近遇到了一个类似的问题,并能够用以下方法解决它:

用户.rb

# Indexes
#  index_users_on_confirmation_token    (confirmation_token) UNIQUE
#  index_users_on_email                 (email) UNIQUE
#  index_users_on_reset_password_token  (reset_password_token) UNIQUE
#  index_users_on_slug                  (slug) UNIQUE
#  index_users_on_unlock_token          (unlock_token) UNIQUE

class User < ApplicationRecord
  has_many  :user_roles, -> { includes(:role).order(:role_id) },
            inverse_of: :user,
            dependent: :destroy

  accepts_nested_attributes_for :user_roles,
                                reject_if: proc { |attributes|
                                  attributes['role_id'].blank?
                                }

  has_many  :roles,
            through: :user_roles,
            inverse_of: :user_roles

  validates :first_name,
            presence: true

  validates :last_name,
            presence: true

  validates :email,
            presence: true

  validates :user_roles,
            presence: true
end

用户角色.rb

# Indexes
#  index_user_roles_on_role_id              (role_id)
#  index_user_roles_on_user_id              (user_id)
#  index_user_roles_on_user_id_and_role_id  (user_id,role_id) UNIQUE

# Foreign Keys
#  fk_rails_...  (role_id => roles.id)
#  fk_rails_...  (user_id => users.id)

class UserRole < ApplicationRecord
  belongs_to  :user,
              inverse_of: :user_roles

  belongs_to  :role,
              inverse_of: :user_roles

  validates :user,
            presence: true

  validates :role,
            presence: true
end

role.rb

# Indexes
#  index_roles_on_name  (name) UNIQUE

class Role < ApplicationRecord
  has_many  :user_roles,
            autosave: true,
            inverse_of: :role,
            dependent: :nullify

  has_many  :users,
            through: :user_roles,
            inverse_of: :user_roles
end

factories/user.rb

FactoryBot.define do
  factory :user
    first_name { Faker::Name.first_name }
    last_name { Faker::Name.last_name }
    email { Faker::Internet.email(name: [first_name, last_name].join(' ')) }
    password { 'password123!' }

    trait :approver do
      before(:create) do |user|
        user.user_roles << build(:user_role, :approver_role)
      end
    end

    trait :manager do
      before(:create) do |user|
        user.user_roles << build(:user_role, :manager_role)
      end
    end
  end
end

工厂/user_role.rb

FactoryBot.define do
  factory :user_role do
    user
    role

    trait :approver_role do
      role { create(:role, :approver) }
    end

    trait :manager_role do
      role { create(:role, :manager) }
    end
  end
end

工厂/role.rb

FactoryBot.define do
  factory :role
    trait :approver do
      to_create do |instance|
        instance.attributes = Role.find_or_create_by!({
          name: 'approver'
        }).attributes

        instance.instance_variable_set('@new_record', false)
      end
    end

    trait :manager do
      to_create do |instance|
        instance.attributes = Role.find_or_create_by!({
          name: 'manager',
        }).attributes

        instance.instance_variable_set('@new_record', false)
      end
    end
  end
end

用户规范rb

RSpec.describe User do
  describe 'create' do
    context 'a user with the approver role' do
      subject(:approver_user) { create(:user, :approver) }

      it 'creates a user which has the approver role' do
        expect(approver_user).to be_kind_of(User)
        expect(approver_user.roles.count).to eq(1)
        expect(approver_user.roles.first.name).to eq('approver')
      end
    end

    context 'a user with the approver & manager role' do
      subject(:approver_user) { create(:user, :approver, :manager) }

      it 'creates a user with both approver & manager roles' do
        expect(approver_user).to be_kind_of(User)
        expect(approver_user.roles.count).to eq(2)
        expect(approver_user.roles.first.name).to eq('approver')
        expect(approver_user.roles.last.name).to eq('manager')
      end
    end
  end
end

相关问题