测试无DB查询方法:构建带has_many through关联的预加载记录遇阻
Got it, let's work through this problem—building a preloaded record with a has_many :through association without touching the database is totally doable, but we need to account for how Active Record handles these indirect associations.
First, let's break down why your initial attempt didn't work: when you pass hmtas: build_list(...) directly to build(:test_record), Active Record doesn't automatically create the required join model records that glue your test_record to the hmtas entries. Since everything is in-memory (not persisted), it can't infer or create those join links behind the scenes.
Here are two clean, database-free solutions:
1. Manually Build the Join Models
If you want to keep things explicit in your test, construct the join records alongside your main and associated objects, then wire them together:
Assuming your models look like this (adjust names to match your code):
# app/models/test_record.rb class TestRecord < ApplicationRecord has_many :test_record_hmtas, dependent: :destroy # join model has_many :hmtas, through: :test_record_hmtas end # app/models/test_record_hmta.rb class TestRecordHmta < ApplicationRecord belongs_to :test_record belongs_to :hmta end # app/models/hmta.rb class Hmta < ApplicationRecord has_many :test_record_hmtas has_many :test_records, through: :test_record_hmtas end
Your test code would become:
let(:hmtas) { build_list(:has_many_through_association, N) } # Build join models that link each hmta to the test_record let(:join_records) { hmtas.map { |hmta| build(:test_record_hmta, hmta: hmta) } } let(:preloaded_record) { build(:test_record, test_record_hmtas: join_records) }
This creates all objects in memory, and the test_record will correctly reference the hmtas via the join records—no database queries needed. You can verify with preloaded_record.hmtas.loaded? (it should return true, confirming no lazy loading will happen).
2. Add a Transient Attribute to Your Factory
For a cleaner, reusable approach, update your test_record factory to handle the has_many :through association automatically during build:
# spec/factories/test_records.rb FactoryBot.define do factory :test_record do # Your test_record attributes here # Transient attribute to control how many hmtas to build transient do hmtas_count { 0 } end after(:build) do |test_record, evaluator| next if evaluator.hmtas_count.zero? # Build hmtas and their corresponding join records hmtas = build_list(:has_many_through_association, evaluator.hmtas_count) join_records = hmtas.map do |hmta| build(:test_record_hmta, hmta: hmta, test_record: test_record) end test_record.test_record_hmtas = join_records end end end
Now in your test, you can build the preloaded record in one line:
let(:preloaded_record) { build(:test_record, hmtas_count: N) }
This keeps your test code concise and encapsulates the association logic in the factory, where it belongs.
The key takeaway: has_many :through relies on that middle join model, so you can't skip building it even in memory. By explicitly creating those join records, you establish the association without ever hitting the database.
内容的提问来源于stack exchange,提问作者Matthias Michael Engh

