#158 Factories not Fixtures
- Download:
- source codeProject Files in Zip (118 KB)
- mp4Full Size H.264 Video (20 MB)
- m4vSmaller H.264 Video (13.2 MB)
- webmFull Size VP8 Video (36.5 MB)
- ogvFull Size Theora Video (27.1 MB)
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) <span class="passed">.....</span> Finished in 0.217478 seconds <span class="passed">5 examples, 0 failures</span>
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) <span class="passed">...</span><span class="failed">FF</span> 1) <span class="failed">ActiveRecord::RecordInvalid in 'User should authenticate with matching username and password'</span> 2) <span class="failed">ActiveRecord::RecordInvalid in 'User should not authenticate with incorrect password' Validation failed: Username has already been taken, Email is invalid</span> Finished in 0.167193 seconds <span class="failed">5 examples, 2 failures</span>
…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) <span class="passed">.....</span> Finished in 0.163722 seconds <span class="passed">5 examples, 0 failures</span>
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.