Speed up rspec tests - part2

Kamil Baćkowski

Our relations

Relations diagram

1. Factory associations

FactoryGirl.define do
  factory :user do
    company
  end

  factory :company do
    subscription
  end

  factory :subscription do
  end

  factory :post do
    user
  end

  factory :comment do
    post
    user
  end
end

Example spec with factories

describe PostsController, type: :controller do
  render_views
  let(:user) { create(:user) }
  let(:post) { create(:post) }
  let(:comment) { create(:comment, post: post) }

  describe '#show' do
    it 'renders post with comments' do
      post
      comment
      login_as user
      get :show, params: { id: post.id }
      # expect ...
    end
  end
end

How many companies, subscriptions & users ?

3 users, 3 companies and 3 subscriptions

How to fix this ?

describe PostsController, type: :controller do
  render_views
  let(:user) { create(:user) }
  let(:post) { create(:post, user: user) }
  let(:comment) { create(:comment, post: post, user: user) }

  describe '#show' do
    it 'renders post with comments' do
      post
      comment
      login_as user
      get :show, params: { id: post.id }
      # expect ...
    end
  end
end

How can we protect ?

FactoryGirl.define do
  factory :company do
    id 1
    subscription

    trait :with_unlocked_id do
      id nil
    end
  end
end

Now creating another company in spec will raise exception unless we explicitly use with_unlocked_id

create(:company)
create(:company, :with_unlocked_id)

Other solution

FactoryGirl.define do
  factory :comment do
    post
    user { post.user }
  end
end

However this will only work if we have more associations, so in our case this can’t be used for post factory.

2. Using seeds to reduce factory objects creation

# spec/support/seed_users.rb
class SeedUsers
  class << self
    def load
      FactoryGirl.create(:user, id: 1, :with_paid_account)
      FactoryGirl.create(:user, id: 2, :with_free_account)
    end

    def find_with_paid_plan
      User.find(1)
    end

    def find_with_free_plan
      User.find(2)
    end
  end
end
config.before(:suite) do
  DatabaseCleaner.strategy = :truncation
  DatabaseCleaner.clean
  SeedUsers.load
end

Usage in specs

describe PostsController, type: :controller do
  render_views
  let(:user) { SeedUsers.find_with_free_plan }
  let(:post) { create(:post, user: user) }
  let(:comment) { create(:comment, post: post) }

  describe '#show' do
    it 'renders post with comments' do
      post
      comment
      login_as user
      get :show, params: { id: post.id }
      # expect ...
    end
  end
end

Pros

  • Can boost speed dramatically especially when having > 1k unit tests
  • Easily in implementation, and we’re still using factories

Cons

  • Changing something in those seeds can brake a lot of specs

What about feature specs ? Will those work ?

Feature specs truncate tables so they will delete our seed data :(

Adjust spec order

# spec/rails_helper.rb
RSpec.configure do |config|
  config.register_ordering(:global) do |items|
    items.sort_by do |group|
      group.metadata[:type] == :feature ? 1 : 0
    end
  end
end

That way feature specs will run at the end and all other specs can make use of seed data

3. Optimize basic factories

FactoryGirl.define do
  factory :post do
    user
    category

    after(:create) do |post|
      2.times { FactoryGirl.create(:comment, post: post) }
    end
  end
end

vs

FactoryGirl.define do
  factory :post do
    user

    trait :with_category do
      category
    end

    trait :with_comments do
      after(:create) do |post|
        2.times { FactoryGirl.create(:comment, post: post) }
      end
    end
  end
end

Summary

Questions ?