homeASCIIcasts

158: Factories Not Fixtures 

(view original Railscast)

Other translations: He

This episode will revisit the topic of creating test objects without using fixtures, which was first covered back in episode 60. This can now be done by making use of factories, and there are a number of factory tools available. We’ll demonstrate some ways in which factories can be used to improve your Rails tests.

We’ll start by looking at the spec for a user model. The spec has two tests in it related to authentication. The first test checks that when the correct username and password is passed to the authenticate method a User object is returned; the second passes a correct username but an incorrect password and checks that the method returns nil.

require File.dirname(__FILE__) + '/../spec_helper'
describe User do
  fixtures :all
  it "should authenticate with matching username and password" do
    User.authenticate('bob', 'secret').should == users(:bob)
  end
  it "should not authenticate with incorrect password" do
    User.authenticate('bob', 'incorrect').should be_nil
  end
end

Note that we’re using fixtures in the tests. Fixtures have several weaknesses that make them less than ideal, but the main problem is that they separate the data we’re testing with from the behaviour we’re testing. In the first test above we’re testing the behaviour of the User model, but we don’t create an actual User, we rely on the data in the fixtures. Relying on fixtures makes tests more brittle and more difficult to read. You have to look at the fixtures file to fully understand the test and even then things don’t always become totally clear.

bob:
  username: bob
  email: bob@example.com
  password_hash: 3488f5f7efecab14b91eb96169e5e1ee518a569f
  password_salt: bef65e058905c379436d80d1a32e7374b139e7b0
  admin: false
admin:
  username: admin
  email: admin@example.com
  password_hash: 3488f5f7efecab14b91eb96169e5e1ee518a569f
  password_salt: bef65e058905c379436d80d1a32e7374b139e7b0
  admin: true

For example, the password for our User model is encrypted, so although we’re checking for the password “secret” in the test, we can’t tell for sure that that’s the correct password from looking at the test data.

Removing The Fixtures

Before we make any changes to our tests we’ll run them to make sure that they currently pass.

$ rake spec
(in /Users/eifion/rails/apps_for_asciicasts/ep158)
.....
Finished in 0.217478 seconds
5 examples, 0 failures

They do, so we can begin to make our changes. Before we start using factories we’ll try creating our objects directly and see how that goes. The first change we’ll make is to remove the dependency on fixtures and to create a user in the database for each test.

require File.dirname(__FILE__) + '/../spec_helper'
describe User do
  it "should authenticate with matching username and password" do
    user = User.create!(:username => "bob", :password => "secret")
    User.authenticate('bob', 'secret').should == user
  end
  it "should not authenticate with incorrect password" do
    user = User.create!(:username => "bob", :password => "secret")
    User.authenticate('bob', 'incorrect').should be_nil
  end
end

Now we run the tests again and…

$ rake spec
(in /Users/eifion/rails/apps_for_asciicasts/ep158)
...FF
1)
ActiveRecord::RecordInvalid in 'User should authenticate with matching username and password'
2)
ActiveRecord::RecordInvalid in 'User should not authenticate with incorrect password'
Validation failed: Username has already been taken, Email is invalid
Finished in 0.167193 seconds
5 examples, 2 failures

…this time we see two failures. It seems from the second failure that our User model has an email field that has some validation against it. We could go back and add this field to the tests, and for our two tests this wouldn’t take long, but if we had dozens of tests that used a model this would involve a lot of work. If at some point we added another field to the User model that had validation we’d have to change every test that created a user. In some cases we might have to add data for a field that the test isn’t concerned with. For example our two tests above don’t test the email field and don’t care what value we give for that field but we still have to supply one.

Using Factories

We can solve this problem by using factories. A factory can be used to create valid default model objects for our tests. We can then modify the object’s attributes to create objects that are relevant to the test they’re in.

There are a number of factory plugins available, but for this episode we’re going to use Factory Girl. To install Factory Girl we need to add the following line to /config/environments/test.rb.

config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :source => "http://gems.github.com"

Once that’s done, we’ll run rake to make sure that the gem is installed.

$ sudo rake gems:install RAILS_ENV=test
(in /Users/eifion/rails/apps_for_asciicasts/ep158)
gem install thoughtbot-factory_girl --source http://gems.github.com
Successfully installed thoughtbot-factory_girl-1.2.1
1 gem installed
Installing ri documentation for thoughtbot-factory_girl-1.2.1...
Installing RDoc documentation for thoughtbot-factory_girl-1.2.1...

Now that we have the Factory Girl gem installed we can create our first factory. It’s a good idea to keep your factories in one place and, as we’re using RSpec, we’ll create a factories.rb file under our spec directory.

Next we’ll need to make RSpec aware of our factory by making a change to the /spec/spec_helper.rb file. At the top of the file we’ll require the factories.rb file we created.

require File.dirname(__FILE__) + "/factories"

If we were using Test::Unit or Shoulda we’d put the factories.rb file under the test directory and add the line above to /test/test_helper.rb.

With that done we can create our first factory, the one for our User model.

Factory.define :user do |f|
  f.username "foo"
  f.password "foobar"
  f.password_confirmation { |u| u.password }
  f.email "foo@example.com"
end

We define a factory object with Factory.define and pass it the name of the model, in this case :user, and a block which takes a factory object. In the block we can call attributes on the object to set their default values. In the user factory above we’ve defined four attributes. The username, password and email attributes have been given string values but for the password_confirmation field we’ve had to do something a little different. If we set the password_confirmation field’s value to “foobar” then we’d have to make sure we’d changed both fields every time we wanted to create an object with a different password. Instead we’ve passed a block which takes the current object and set the confirmation to match whatever the password is. This will ensure that the password and confirmation always match.

Now that we’ve defined our User factory, we can modify our tests to make use of factory objects. Instead of creating user objects for tests directly with User.create! we create them from our factory.

require File.dirname(__FILE__) + '/../spec_helper'
describe User do
  it "should authenticate with matching username and password" do
    user =  Factory.create(:user, :username => "frank", :password => "secret")
    User.authenticate("frank", "secret").should == user
  end
  it "should not authenticate with incorrect password" do
    user = Factory.create(:user, :username => "frank", :password => "secret")
    User.authenticate("frank", "incorrect").should be_nil
  end
end

We’re now using Factory.create to create our users, passing first the type of the object we want to create, and then a list of the parameters that we want to change from the defaults. (Note that we’ve changed the username from Bob to Frank so that there’s no clash with the data originally generated by the fixtures.)

Running our tests again, they all pass.

$ rake spec
(in /Users/eifion/rails/apps_for_asciicasts/ep158)
.....
Finished in 0.163722 seconds
5 examples, 0 failures

Creating Sequences

Our User model has a number of validations and the factory objects generally cope well with them, except for one:

  validates_uniqueness_of :username, :email, :allow_blank => true

Our User model requires a unique username and email so we can’t write a test that creates more than one user as we have hard-coded the values for these fields. Factory Girl provides us with a way of using sequences so that each factory object created has a unique value.

Factory.define :user do |f|
  f.sequence(:username) { |n| "foo#{n}" } 
  f.password "foobar"
  f.password_confirmation { |u| u.password }
  f.sequence(:email) { |n| "foo#{n}@example.com" }
end

We’ve now replaced the concrete values for the username and email with a call to the sequence method, which we pass the name of an attribute and a block. The block is passed a number, which we can use in the string value to create a unique value for each attribute. Now when we create a User from our factory it will have unique default username and email attributes.

Associations

As well as a User model, our application has an Article model. Article has a belongs_to relationship to User and a validator that ensures that the article has a user_id.

class Article < ActiveRecord::Base
  belongs_to :user
  has_many :comments, :dependent => :destroy
  validates_presence_of :name, :user_id
  acts_as_list
  def editable_by?(some_user)
    some_user.admin? || some_user == user
  end
end

Factory Girl allows us to define an association in a factory definition by calling association and passing it the name of the associated model.

Factory.define :article do |f|
  f.name "foo"
  f.association :user
end

When an Article object is created it will look for a factory definition that matches :user and automatically build the related object. If our association has a different name, say author, we can explicit say which association should be used.

f.association :author, :factory => :user

Some Final Tips

We’ll finish off our look at Factory Girl by going back to one of our tests and looking at a few more of its features.

it "should authenticate with matching username and password" do
  user =  Factory.create(:user, :username => "frank", :password => "secret")
  User.authenticate("frank", "secret").should == user
end

When we call Factory.create to create an object that object is stored in the database. If we just want to work with it in memory we can use Factory.build instead.

user =  Factory.build(:user, :username => "frank", :password => "secret")

The Factory class also has an attributes_for method which returns a hash of values.

>> Factory.attributes_for :user
=> {:email=>"foo2@example.com", :password=>"foobar", :username=>"foo2", :password_confirmation=>"foobar"}

This is useful in controller tests where you might need a hash of params to pass to a controller action. (Note the sequence values in the email and username fields).

Finally, we can just call Factory directly which has the same effect as using Factory.create.

user =  Factory(:user, :username => "frank", :password => "secret")

We’ve only covered the basic of what Factory Girl can do here. For more information see the documentation pages.

It is also worth taking a look at some of the alternatives to Factory Girl. Machinist3 allows you to define blueprints rather than factories and uses a very concise syntax.

require 'faker'
Sham.name  { Faker::Name.name }
Sham.email { Faker::Internet.email }
Sham.title { Faker::Lorem.sentence }
Sham.body  { Faker::Lorem.paragraph }
User.blueprint do
  name
  email
end
Post.blueprint do
  title
  author
  body
end

Another alternative worth a look is Object Daddy. This takes a different approach in that it adds a generate method to every ActiveRecord model which can be called in your tests to generate a valid model. You can define default values for your test object within the model itself.

class User < ActiveRecord::Base
  generator_for(:start_time) { Time.now }
  generator_for :name, 'Joe'
  generator_for :age => 25
end

Whichever way you choose to generate test objects, factories are an excellent way to improve the tests in your Rails application.