Skip to content

Instantly share code, notes, and snippets.

@tmikeschu
Last active December 24, 2016 16:50
Show Gist options
  • Save tmikeschu/54a8eb596b27c005b3ed89df86145b3a to your computer and use it in GitHub Desktop.
Save tmikeschu/54a8eb596b27c005b3ed89df86145b3a to your computer and use it in GitHub Desktop.
The Messenger: Robust Testing with One Factory Girl Variable

The Messenger: Robust Testing with One Factory Girl Variable

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.

Gem Time

In the testing group:

group :development, :test do
  ...
  gem 'rspec-rails'
  gem 'factory_girl_rails', "~> 4.0"
  gem 'faker'
  gem 'database_cleaner'
  ...
end

Set up the app environment for factory girl (from your project directory).

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.

Configure factory_girl.rb

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.

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

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

Set up factories.rb

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.

RSpec-tacular

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment