TL;DR
FactoryGirl tries to be helpful by making a very large assumption when it
creates it's "stub" objects. Namely, that:
you have an id
, which means you are not a new record, and thus are already persisted!
Unfortunately, ActiveRecord uses this to decide if it should
keep persistence up to date.
So the stubbed model attempts to persist the records to the database.
Please do not try to shim RSpec stubs / mocks into FactoryGirl factories.
Doing so mixes two different stubbing philosophies on the same object. Pick
one or the other.
RSpec mocks are only supposed to be used during certain parts of the spec
life cycle. Moving them into the factory sets up an environment which will
hide the violation of the design. Errors which result from this will be
confusing and difficult to track down.
If you look at the documentation for including RSpec into say
test/unit,
you can see that it provides methods for ensuring that the mocks are properly
setup and torn down between the tests. Putting the mocks into the factories
provides no such guarantee that this will take place.
There are several options here:
Don't use FactoryGirl for creating your stubs; use a stubbing library
(rspec-mocks, minitest/mocks, mocha, flexmock, rr, or etc)
If you want to keep your model attribute logic in FactoryGirl that's fine.
Use it for that purpose and create the stub elsewhere:
stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)
Yes, you do have to manually create the associations. This is not a bad thing,
see below for further discussion.
Clear the id
field
after(:stub) do |order, evaluator|
order.id = nil
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
Create your own definition of new_record?
factory :order do
ignore do
line_items_count 1
new_record true
end
after(:stub) do |order, evaluator|
order.define_singleton_method(:new_record?) do
evaluator.new_record
end
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
end
What's Going On Here?
IMO, it's generally not a good idea to attempt to create a "stubbed" has_many
association with FactoryGirl
. This tends to lead to more tightly coupled code
and potentially many nested objects being needlessly created.
To understand this position, and what is going on with FactoryGirl, we need to
take a look at a few things:
- The database persistence layer / gem (i.e.
ActiveRecord
, Mongoid
,
DataMapper
, ROM
, etc)
- Any stubbing / mocking libraries (mintest/mocks, rspec, mocha, etc)
- The purpose mocks / stubs serve
The Database Persistence Layer
Each database persistence layer behaves differently. In fact, many behave
differently between major versions. FactoryGirl tries to not make assumptions
about how that layer is setup. This gives them the most flexibility over the
long haul.
Assumption: I'm guessing you are using ActiveRecord
for the remainder of
this discussion.
As of my writing this, the current GA version of ActiveRecord
is 4.1.0. When
you setup a has_many
association on it,
there's
a
lot
that
goes
on.
This is also slightly different in older AR versions. It's very different in
Mongoid, etc. It's not reasonable to expect FactoryGirl to understand the
intricacies of all of these gems, nor differences between versions. It just so
happens that the has_many
association's writer
attempts to keep persistence up to date.
You may be thinking: "but I can set the inverse with a stub"
FactoryGirl.define do
factory :line_item do
association :order, factory: :order, strategy: :stub
end
end
li = build_stubbed(:line_item)
Yep, that's true. Though it's simply because AR decided not to
persist.
It turns out this behavior is a good thing. Otherwise, it would be very
difficult to setup temp objects without hitting the database frequently.
Additionally, it allows for multiple objects to be saved in a single
transaction, rolling back the whole transaction if there was a problem.
Now, you may be thinking: "I totally can add objects to a has_many
without
hitting the database"
order = Order.new
li = order.line_items.build(name: 'test')
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 1
li = LineItem.new(name: 'bar')
order.line_items << li
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 2
li = LineItem.new(name: 'foo')
order.line_items.concat(li)
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 3
order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 5
Yep, but here order.line_items
is really an
ActiveRecord::Associations::CollectionProxy
.
It defines it's own build
,
#<<
,
and #concat
methods. Of, course these really all delegate back to the association defined,
which for has_many
are the equivalent methods:
ActiveRecord::Associations::CollectionAssocation#build
and ActiveRecord::Associations::CollectionAssocation#concat
.
These take into account the current state of the base model instance in order
to decide whether to persist now or later.
All FactoryGirl can really do here is let the behavior of the underlying class
define what should happen. In fact, this lets you use FactoryGirl to
generate any class, not
just database models.
FactoryGirl does attempt to help a little with saving objects. This is mostly
on the create
side of the factories. Per their wiki page on
interaction with ActiveRecord:
...[a factory] saves associations first so that foreign keys will be properly
set on dependent models. To create an instance, it calls new without any
arguments, assigns each attribute (including associations), and then calls
save!. factory_girl doesn’t do anything special to create ActiveRecord
instances. It doesn’t interact with the database or extend ActiveRecord or
your models in any way.
Wait! You may have noticed, in the example above I slipped the following:
order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 5
Yep, that's right. We can set order.line_items=
to an array and it isn't
persisted! So what gives?
The Stubbing / Mocking Libraries
There are many different types and FactoryGirl works with them all. Why?
Because FactoryGirl doesn't do anything with any of them. It's completely
unaware of which library you have.
Remember, you add the FactoryGirl syntax to your test library of choice.
You don't add your library to FactoryGirl.
So if FactoryGirl isn't using your preferred library, what is it doing?
The Purpose Mocks / Stubs Serve
Before we get to the under the hood details, we need to define what
a
"stub"
is
and its intended purpose:
Stubs provide canned answers to calls made during the test,