Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Speeding up associations in model specs with FactoryGirl - create vs build vs build_stubbed

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?

like image 791
GMA Avatar asked Dec 18 '13 16:12

GMA


4 Answers

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

like image 98
Nesha Zoric Avatar answered Nov 07 '22 08:11

Nesha Zoric


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)
like image 29
usha Avatar answered Nov 07 '22 07:11

usha


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).

like image 40
Joe Ferris Avatar answered Nov 07 '22 07:11

Joe Ferris


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

like image 43
Dennis Avatar answered Nov 07 '22 06:11

Dennis