#158 Factories not Fixtures (revised)
- Download:
- source codeProject Files in Zip (88.7 KB)
- mp4Full Size H.264 Video (21.1 MB)
- m4vSmaller H.264 Video (11 MB)
- webmFull Size VP8 Video (13.2 MB)
- ogvFull Size Theora Video (25.2 MB)
As you write tests for your Rails applications it’s good practice to keep each test isolated and minimize the number of external dependencies it has. Fixtures go against this idea as they add an external data dependency that each test relies on. In this episode we’ll look at replacing fixtures with factories.
The Problem With Fixtures
Below are some specs that test an authenticate
method in a User
model. The first checks that when a valid username and password is passed to authenticate
the matching user is returned while the second makes sure that nil
is returned if invalid details are passed in.
require 'spec_helper' describe User do fixtures :all it "authenticates with matching username and password" do User.authenticate("batman", "secret").should eq(users(:batman)) end it "does not authenticate with incorrect password" do User.authenticate("batman", "incorrect").should be_nil end end
We don’t create any users in the tests to authenticate against in these test so how where do they get this information from? The answer is that these users are defined in a fixtures file and the tests load them from there.
batman: username: batman email: batman@example.com password_digest: "$2a$10$uh/MLjEjRXyKK9jZLFld7OMmaqP9o3uPC8jgr6iebMdD.hpcVfKwe" admin: false admin: username: admin email: admin@example.com password_digest: "$2a$10$uh/MLjEjRXyKK9jZLFld7OMmaqP9o3uPC8jgr6iebMdD.hpcVfKwe" admin: true
There’s a heavy dependency between the tests and the fixtures and we can cause the tests to fail just by changing the fixtures. External dependencies that are this strong aren’t a good idea and make the tests brittle. Another problem here is that the passwords are hashed so it’s not at all obvious that the first user above has the password “secret”.
Introducing Factories
How can we go about removing our dependency on fixtures? One solution is to remove them and to create the users we need for each test in the test itself, like this:
it "authenticates with matching username and password" do user = User.create(username: "batman", password: "secret") User.authenticate("batman", "secret").should eq(user) end it "does not authenticate with incorrect password" do user = User.create(username: "batman", password: "secret") User.authenticate("batman", "incorrect").should be_nil end
If we run the test suite now, these tests will fail. This is because we have validations in the User
model that we haven’t satisfied in the users we’ve created. There’s a whole list of validations in the User model that we need to keep in mind every time we create a user in a spec and if we add any more validations later on we run the risk of breaking existing specs.
class User < ActiveRecord::Base attr_accessible :username, :email, :password, :password_confirmation has_secure_password validates_presence_of :username validates_uniqueness_of :username, :email, allow_blank: true validates_format_of :username, with: /^[-\w\._@]+$/i, allow_blank: true, message: "should only contain letters, numbers, or .-_@" validates_format_of :email, with: /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i validates_presence_of :password, on: :create validates_length_of :password, minimum: 4, allow_blank: true def self.authenticate(username, password) user = find_by_username(username) return user if user && user.authenticate(password) end def can_manage_article?(article) admin? || article.user == self end end
This is where factories come in useful. There are a number of factory frameworks available but here we’re going to use Factory Girl. To install it we just need to add it to the :test
group in our application’s Gemfile
and then run bundle
. In a Rails application we should use the factory_girl_rails
gem which will install factory_girl
as a dependency.
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'bcrypt-ruby' gem 'rspec-rails', :group => [:test, :development] group :test do gem 'capybara' gem 'factory_girl_rails' end
There’s some excellent documentation in Factory Girl’s Getting Started file and one of the things it shows us is where we can put our factories. These can go in a single factories.rb
file under the /test
or /spec
directories or in any Ruby file under /test/factories
or /spec/factories
. We’ll take the single file approach and, as we’re using RSpec, our factories will go in /spec/factories.rb
. Here’s the factory for User
.
FactoryGirl.define do factory :user do username "foo" password "foobar" email { "#{username}@example.com" } end end
To create factories we need to call FactoryGirl.define
and pass it a block. Inside this block we use the factory
method to define a factory, passing it the name of the model and another block. In this inner block we define default values for each of the model’s attributes and it’s sensible to set the defaults so that the model’s validations will pass. In the case of our User
model we need a valid username
, password
and email
. We can define attributes based on the value of another attribute if we want to and here we’ve based the email address on the username.
In our specs we can now create users from the factory we’ve created by calling FactoryGirl.create
, passing in the name of the factory we want to create and any parameters we want to override from their default values.
it "authenticates with matching username and password" do user = FactoryGirl.create(:user, username: "batman", password: "secret") User.authenticate("batman", "secret").should eq(user) end it "does not authenticate with incorrect password" do user = FactoryGirl.create(:user, username: "batman", password: "secret") User.authenticate("batman", "incorrect").should be_nil end
When we run our specs now they both pass again and they no longer have any dependencies on external fixtures. There’s a potential problem with our factory, however. Each user we create from the factory will have the same username
and password
and these fields both have validates_uniqueness_of
validators. Any test we write that creates more than one user will throw an error.
We can solve this by using a numeric sequence when we create each new user so that the name is unique, like this:
FactoryGirl.define do factory :user do sequence(:username) { |n| "foo#{n}" } password "foobar" email { "#{username}@example.com" } end end
This means that every user that’s created now will have a unique name and email address.
A Quick Tip
Calling FactoryGirl
every time we want to create a factory can quickly become tedious. We can shorten this by adding the following line inside the config
block of our spec_helper
file. (This works in the test_helper
file too if you’re using Test::Unit.)
config.include FactoryGirl::Syntax::Methods
With this line in place we can remove FactoryGirl
and call create
directly.
it "authenticates with matching username and password" do user = create(:user, username: "batman", password: "secret") User.authenticate("batman", "secret").should eq(user) end
Note that a build
method also exists if we want to create a model without saving it.
Handling Associations
Factory Girl also works well across associations. In our app we have an Article
model that has a belongs_to
relationship with User
and which validates the presence of the user_id
. This ensures that every article that’s created belongs to a user.
class Article < ActiveRecord::Base belongs_to :user validates_presence_of :name, :user_id end
When we create a factory for articles we need to give each article an associated user and Factory Girl makes this simple. All we need to do is call user
and it will automatically assign a new user based on the User
factory.
FactoryGirl.define do factory :user do sequence(:username) { |n| "foo#{n}" } password "foobar" email { "#{username}@example.com" } end factory :article do name "Foo" user end end
If we need to customize this behaviour we can use the association
method and pass in options such as which factory to use for the association. The Getting Started page we linked to earlier has more details about this. We can see this association in action by adding a new spec that uses it.
it "can mangage articles he owns" do article = create(:article) user = article.user user.can_manage_article?(article).should be_true user.can_manage_article?(create(:article)).should be_false end
In this spec we create an article from a factory this automatically creates a user for that article. We can then use this to check that the user can manage his own article but not any others.
Creating Factories Based on Other Factories
Our User
model has an admin
attribute that is false
by default. When we’re writing tests to test the admin parts of the site we want to create admin users and it would be good if we didn’t need to override this default value each time. Factory Girl lets us create factories based on others and so we can create an admin
factory, nested inside the user factory, and override the attributes that we want to change.
factory :user do sequence(:username) { |n| "foo#{n}" } password "foobar" email { "#{username}@example.com" } admin false factory :admin do admin true end end
Each time we create an admin
factory now a new User
will be created but with admin
being true.
it "can manage any articles as admin" do create(:admin).can_manage_article?(create(:article)).should be_true create(:user).can_manage_article?(create(:article)).should be_false end
An admin user should be able to manage any article, and the spec above will test for this correctly.
Speeding Up Our Test Suite
It’s a bad habit to use the create
method every time we create a model object as this saves a record to the database. Often a test doesn’t require this so we’re making it run more slowly than it potentially could. It’s always worth trying to use the build
method first to see if that will work. In certain cases it won’t, such as in our authentication tests which fetch a user from the database. In these cases we need to stick with create
.
require 'spec_helper' describe User do it "authenticates with matching username and password" do user = create(:user, username: "batman", password: "secret") User.authenticate("batman", "secret").should eq(user) end it "does not authenticate with incorrect password" do user = create(:user, username: "batman", password: "secret") User.authenticate("batman", "incorrect").should be_nil end it "can mangage articles he owns" do article = build(:article) user = article.user user.can_manage_article?(article).should be_true user.can_manage_article?(create(:article)).should be_false end it "can manage any articles as admin" do build(:admin).can_manage_article?(create(:article)).should be_true build(:user).can_manage_article?(create(:article)).should be_false end end
We should also be on the lookout for times when we can skip using a factory entirely. For example in the spec where we check that a user can’t manage an article he didn’t create we don’t care what attributes the article has and we can just create a new Article
.
it "can mangage articles he owns" do article = build(:article) user = article.user user.can_manage_article?(article).should be_true user.can_manage_article?(Article.new).should be_false end
For the cases when we need anything other than a brand new model object we should stick with the factories, however.
That’s it for our look at Factory Girl. We haven’t covered everything that it does but the documentation covers everything well.
Alternatives
If you’re looking for an alternative to Factory Girl then you should take a look at Fabrication. This has a very similar syntax and feature set to Factory Girl but has a few key differences. For example it does some lazy generation for associations. If you find yourself working with associations a lot and have a complex association tree where Factory Girl seems to load too much at once then Fabrication may be the better solution.