Prologue: "The Messenger came along and changed everything. Testing will never be the same." -Elon Musk (probably)
We all know test-driven development is essential for high quality applications. We also know that we want at least somewhat robust data in our testing environment. What is the best way to populate test data that simulates production level databases? Well, I don't know about best, but in the rest of this article I'll show you a way that I find particularly beautiful. Time to meet The Messenger.
The context of this illustration consists of:
- a Rails application,
- using an RSpec test suite,
- with Factory Girl, Database Cleaner, and Faker in the Gemfile
A prerequisite for this walk-through is a fully functional relational database with association methods.
Let's get down to testing.
In the testing group:
group :development, :test do
...
gem 'rspec-rails'
gem 'factory_girl_rails', "~> 4.0"
gem 'faker'
gem 'database_cleaner'
...
end
bundle install
mkdir spec/support
touch spec/support/factory_girl.rb
touch spec/factories.rb
Lastly, in your rails_helper.rb
file, uncomment the line:
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
This auto requires your factory set up in your testing suite. If you end up expanding your test folder,
it might be worth commenting this out again and adding require 'spec/support'
instead.
When I first see configuration related snippets, I have a mild panic attack. All this new jargon comes into the picture, and I wonder if I actually know programming. In case you're like me, let's walk through how this set up works.
-
I want to keep my testing database clean and brand new each time I run the test. So, I recommend adding this to the top of the file:
DatabaseCleaner.strategy = :truncation
-
We want to configure our RSpec to include Factory Girl and clean the database.
RSpec.configure do |c| #
c.include FactoryGirl::Syntax::Methods
c.before(:all) do # Here, I'm telling RSpec, "Hey, before each test, do this:"
begin
DatabaseCleaner.clean
FactoryGirl.lint
ensure # Can you tell I really like a clean database?
DatabaseCleaner.clean
end
end
c.after(:each) do # "Hey, after each test, also do this:"
DatabaseCleaner.clean
end
end
Up to now, the database schema really didn't influence our set up. Now, we set up our factories based on the existing relationships we have in the database. This example is from an application I made for a single user to track information regarding a job hunt.
At a high level:
- A company has many jobs, and a job belongs to a company
- A company has many contacts, and a contact belongs to a company.
- A job has many comments, and a comment belongs to a job.
- A category has many jobs, and a job belongs to one category.
Additionally, a heads up on two Factory Girl methods you will see: create(:factory)
and create_list(:factory, number)
These are pretty intuitive: they make an instance of the factory named, or a list of the number of instances specified in the list argument.
With that in mind, we can make our factory to simulate this relational world:
Spoiler: Faker is going to pop up in here. It is already available for us through our gem file.
require 'factory_girl_rails'
FactoryGirl.define do
factory :contact do # :contact represents an instance from the Contacts table.
sequence :first_name do # first_name is an attribute in the Contacts table
Faker::Name.first_name # With sequence, we are creating a random Faker name each time a contact is made.
end
sequence :last_name do # I prefer to sequence everything.
Faker::Name.last_name # check out the Faker gem docs for more info on the available modules.
end
sequence :position do
Faker::Name.title
end
sequence :email do
Faker::Internet.email
end
end
# if an attribute is validated for uniqueness, sequence it with an incrementing counter (n) and interpolate it in a string.
factory :category do # Whatever symbol is on the factory line is an object that can be made with create and create_list
sequence :title do |n|
"#{Faker::Company.name} #{n}"
end
end
factory :job do
sequence :title do
Faker::Name.title
end
sequence :description do
Faker::Lorem.sentence
end
sequence :level_of_interest do
rand(99) + 1
end
sequence :city do
Faker::Address.city
end
factory :job_with_category do # Association alert! This is how we capitalize on a job having many categories.
after(:create) do |job|
job.category = create(:category)
end
end
# The above is saying, "Hey, after a job is made, assign it a category from the category factory.
end
factory :company do
sequence :name do |n|
"#{Faker::Company.name} #{n}"
end
factory :company_with_jobs_and_contacts do # THE MESSENGER
after(:create) do |company|
company.jobs = create_list(:job_with_category, 10) # note :job_with_category vs. just :job
company.contacts = create_list(:contact, 10)
end
end
end
end
The Messenger in this case is :company_with_jobs_and_contacts
. When we create this, we have access to data
from every table in the database except comments: companies, jobs, contacts, and categories.
I could have easily made something like a :category_with_jobs
, where a list of jobs with companies that have contacts
get attached to category.jobs
in its after(:create)
line. I chose companies because it made the most sense to me.
As another side note, I could have made a comment factory, like:
factory :comment do
sequence :content do
Faker::Lorem.sentence
end
end
And then, in my job factory, make something like:
factory :job_with_category_and_comments do
after(:create) do |job|
job.comments = create_list(:comment, 5)
job.category = create(:category)
end
end
And use that in my :company_with_jobs_and_contacts
factory.
Whew, those factories were a bit of work, right? It's all worth it, trust me. Now, in our test files, we can do something like this:
RSpec.feature "User can do something" do
before do
@companies = create_list(:company_with_jobs_and_contacts, 10)
end
...
end
This will set @companies
to an ActiveRecord collection of company objects, each with association data.
Here is a list of some of the things you can do with this Messenger (referring to @companies
above) in a before block, or in specific tests:
@company = @companies.first
@jobs = @company.jobs
@job = @jobs[3]
@comment = @job.comments.first
@category = @job.category
Wow - an entire (mini) database in just one factory creation! How very nice and DRY. Instead of creating multiple lists in our before block, we create one integrated factory, and extract what we need for the test at hand. Beautiful. The next time you find yourself repeating any kind of create methods, whether ActiveRecord or Factory Girl, consider putting in some extra pre-work in the factories, and let The Messenger take care of the rest.
Happy testing!