Say I have models User
and Post
, a user has_many
posts and a post belongs_to
a user.
When I write a spec for Post
, my first instinct is to write something like this:
before do
@user = FactoryGirl.create :user
@post = @user.posts.new(title: "Foo", content: "bar)
end
... tests for @post go here ...
But this is going to create a new User - hitting the database - for every single test, which is going to slow things down. Is there a better way to do this that will speed my tests up and avoid hitting the DB so often?
As I understand it, I can't use FactoryGirl.build :user
because, even though it won't hit the DB, the associations won't work properly because @user
won't have an ID and so @post.user
won't work (it returns nil
.)
I could use FactoryGirl.build_stubbed :user
which created a "fake persisted" @user
which does have an ID, but @post.user
still returns nil. Does build_stubbed
have any practical advantage over build
when I'm testing things related to associations?
I suppose I could use build_stubbed
stub @post.user
so it returns @user
... is there any reason this might be a bad idea?
Or should I just use create
and accept the speed hit?
The only other alternative I can think of would be to set up @user in a before(:all)
block which seems like a bad idea.
What's the best way to write these kind of tests in a clean, concise way that avoids making too many DB queries?
A quick explanation of differences: FactoryGirl.create will create new object and associations (if the factory has any) for it. They will all be persisted in db. Also, it will trigger both model and database validations. Callbacks after(:build) and after(:create) will be called after the factory is saved. Also before(:create) will be called before the factory is saved.
FactoryGirl.build won't save an object, but will still make requests to a database if the factory has associations. It will trigger validations only for associated objects. Callback after(:build) will be called after the factory is built.
FactoryGirl.build_stubbed does not call database at all. It creates and assigns attributes to an object to make it behave like an instantiated object. It provides a fake id and created_at. Associations, if any, will be created via build_stubbed too. It will not trigger any validations.
Read full explanation here
If you don't want your tests to be hitting the database, this is what you would have to do.
before do
@user = FactoryGirl.build_stubbed :user
@post = FactoryGirl.build_stubbed :post
@user.stub(:posts).and_return([@post])
@post.stub(:user).and_return(@user)
end
Note: Be careful when using before(:all)
. It doesn't get executed in a transaction. So whatever you create in before(:all)
will get left behind in the database and might cause conflict with other tests
About FactoryGirl.build
, it builds the object, but creates the associations.
For eg:
factory :user do
association posts
end
FactoryGirl.build(:user) #this creates posts in the database even though you are only building the parent object(user)
Short Answer
@user = FactoryGirl.build_stubbed(:user)
@post = FactoryGirl.build_stubbed(:post, :user => @user)
This will make @post.user work without ever hitting the database.
Long Answer
My recommendation would be to wait on the before
block until you're sure you need it. Instead, build the data you need for each individual test and extract duplication to methods or new factories as you find it.
Also, do you actually need to reference the user in every single test? Having @user
available in every test says to other developers that it's important everywhere.
Lastly, assuming that the user association is also declared in your post factory, you'll automatically get a working post.user
when you do build_stubbed(:post)
.
It can be easy to forget the differences between create
, build
, and build_stubbed
. Here's a quick reference for those in the same situation (as this page ranks highly in search results).
# Returns a User instance that's not saved (does not write to DB)
user = build(:user)
# Returns a saved User instance (writes to DB)
user = create(:user)
# Returns a hash of attributes that can be used to build a User instance
attrs = attributes_for(:user)
# Returns an object with all defined attributes stubbed out
stub = build_stubbed(:user)
# Passing a block to any of the methods above will yield the return object
create(:user) do |user|
user.posts.create(attributes_for(:post))
end
Source
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With