Refactoring Specs to Include Factories
In this lesson we're going to refactor our specs to include factories to make our tests more efficient and streamline our codebase.
Guide Tasks
  • Read Tutorial

Before we go on I think it's important that we pause and refactor a part of our application. If you look through our specs do you notice how many times we're creating new records in the database? Running rspec shows that we have 23 specs and the majority of them are creating records in the database, example: Topic.create. This isn't a horrible amount, however it's not a good practice to use the ActiveRecord create method in tests. Why? Good question, let's pretend we're done with the application and we have 100+ specs. What happens if we want to add a required parameter to our Topic model? We would need to go through and alter each of the create calls in our specs, that's not how we're going to live our coding lives #frownyface.

What would be a better approach? Well that's where factories come into play. We can declare some sample factories for each of our models and then we'll only need to update a single file whenever we add or remove attributes from a model, this will also help make our tests run faster.

We're going to implement the FactoryGirl Rails gem, you can use this factory_girl_rails guide for an in depth review of the gem.

To get started, let's update the Gemfile's test/development gem block:

# Gemfile

group :development, :test do
  gem 'byebug'
  gem 'rspec-rails', '~> 3.0'
  gem 'capybara'
  gem 'database_cleaner'
  gem 'factory_girl_rails', '~> 4.5'
end

After running bundle we're almost able to use the FactoryGirl Rails methods. The final configuration item to setup is to add another config option in the rails_helper file:

# spec/rails_helper.rb

config.include FactoryGirl::Syntax::Methods

Now we're all setup, let's create a new directory in the spec folder:

mkdir spec/factories

And now let's create a factory for our Topic model:

touch spec/factories/topics.rb

This is the standard naming convention for FactoryGirl and you'll see that when we run future model or resource generators FactoryGirl will automatically create sample factories for us. Let's update this file:

# spec/factories/topics.rb

FactoryGirl.define do
  factory :topic do
    title 'Sports'
  end
end

We can test to see if this is working by running the rails console in the test environment (we need to start the rails console in test mode since the factory girl gem only loads in the test environment):

rails c -e test

Now let's create a test factory:

FactoryGirl.create(:topic)

Running this will create the test factory with the parameters we supplied in the topics factory file.

large

Ok, now that we know this is working we can replace all of our create specs with our new FactoryGirl calls. Yikes, we have an issue with the features/topic_spec test, it's creating a list of Topics. No need to worry, FactoryGirl let's us create as many factories as we want, let's update the factories/topics.rb file:

# spec/factories/topics.rb

FactoryGirl.define do
  factory :topic do
    title 'Sports'
  end

  factory :second_topic, class: 'Topic' do
    title 'Coding'
  end
end

Now we can swap out Topic.create(title: "Coding") with FactoryGirl.create(:second_topic) and get the same result. Let's test this out by running rspec.

Everything is still passing, that's always a good sign for a refactor. Now let's create factories for our other models (we haven't used any create methods for posts yet, but we will need this later on). Create posts.rb and users.rb files in the factories directory and fill them in with the following code:

# spec/factories/posts.rb

FactoryGirl.define do
  factory :post do
    title 'My Great Post'
    content 'Amazing content'
    topic
    user
  end

  factory :second_post, class: 'Post' do
    title 'Another Guide'
    content 'Killer post'
    topic
    user
  end
end
# spec/factories/users.rb

FactoryGirl.define do
  factory :user do
    email "test@test.com"
    password "password"
    password_confirmation "password"
    first_name "Jon"
    last_name "Snow"
    username "watcheronthewall"
  end

  factory :second_user, class: 'User' do
    email "testy@test.com"
    password "password"
    password_confirmation "password"
    first_name "Jon"
    last_name "Snow"
    username "second_watcheronthewall"
  end
end

You may be rightfully wondering, why didn't we place any values for topic and user in the posts factory. FactoryGirl is smart enough to look at the model relationships and if you have a has_many/belongs_to setup it will automatically assume that you want that factory associated with the factory for the associated models. Before we implement this in the specs, let's run them in the console:

FactoryGirl.create(:user)

Ok, that worked nicely, just for the heck of it let's run it again. Well that gives an ugly error ActiveRecord::RecordInvalid: Validation failed: Email has already been taken, Username has already been taken. This is because we have unique constraints for these values, but not to worry, we can fix this by altering our specs slightly with a call to the FactoryGirl sequence method to ensure any unique values won't run into a duplicate error:

# spec/factories/users.rb

FactoryGirl.define do
  sequence :email do |n|
    "test#{n}@example.com"
  end

  sequence :username do |n|
    "test#{n}"
  end

  factory :user do
    email { generate :email }
    password "password"
    password_confirmation "password"
    first_name "Jon"
    last_name "Snow"
    username { generate :username }
  end

  factory :second_user, class: 'User' do
    email { generate :email }
    password "password"
    password_confirmation "password"
    first_name "Jon"
    last_name "Snow"
    username { generate :username }
  end
end

If you run the console now you'll see that you can create multiple items without errors. With all of that working let's refactor the rest of the specs that are using the create method in the specs.

This gives us one failure on the spec making sure that users can't have the same username:

Failures:

  1) User creation validations should ensure that all usernames are unique
     Failure/Error: expect(duplicate_username_user).to_not be_valid
       expected #<User id: 8, email: "test2@test.com", encrypted_password: "$2a$04$Jr.YrM1FE2XPfahisOfNAuWgc8.A..WUmPczyeDb0IH...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil, first_name: "Joni", last_name: "Snowy", avatar: nil, username: "watcheronthewall", created_at: "2016-02-20 22:46:39", updated_at: "2016-02-20 22:46:39", role: "student"> not to be valid
     # ./spec/models/user_spec.rb:35:in `block (4 levels) in <top (required)>'

Finished in 0.76235 seconds (files took 2.73 seconds to load)
23 examples, 1 failure, 2 pending

Failed examples:

rspec ./spec/models/user_spec.rb:33

We can fix this by updating that spec with a very small change:

# spec/models/user_spec.rb

      it 'should ensure that all usernames are unique' do
        duplicate_username_user = User.create(email: "test2@test.com", password: "password", password_confirmation: "password", first_name: "Joni", last_name: "Snowy", username: @user.username)
        expect(duplicate_username_user).to_not be_valid
      end

See how instead of hardcoding the username we simply swapped that our with the value from the @user instance variable?

Now our specs are all passing and our code is much cleaner. I will do this throughout the course, it's good to pause development from time to time to perform refactors like this. I've had it happen too many times in my career where I was so focused on building features that I didn't spend enough time refactoring and ended up with some messy applications. Since we're following BDD principles Red, Green, Refactor, this approach is built into our development process, however it's also important to occasionally take a step back and perform a high level analysis of the application to ensure you're not falling into any antipatterns.

Resources