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