Skip to content

Backend Training: Rspec Tests

Nelson Lee edited this page Dec 1, 2017 · 7 revisions

🔖 : rspec_rails, factory_bot, faker

We have written a significant amount of code so far it is now an excellent time to add some spec tests.

Factory

Let's start by adding some factories. Factories are generators that help us create the model records. For more info on factories please see factory_bot gem(doc)

⚠️ If your inside your Gemfile you are using factory_girl_rails please upgrade it to factory_bot. You can upgrade following the ↪️ FactoryBot Upgrade Guide

Create post factory

# spec/factories/post.rb
FactoryBot.define do
  factory :post do
    title          { Faker::Lorem.sentence(1) }
    content        { Faker::Lorem.paragraph }
    category_list  { Faker::Lorem.word }
    published      { Faker::Boolean.boolean }
    published_date { nil }
  end
end

Model Specs

Models specs are used to test validations, callbacks, scopes, and relationship.

Add a factory test to post

# spec/models/post_spec.rb
require 'rails_helper'

RSpec.describe Post, type: :model do
  it 'has a valid factory' do
    # Using the shortened version of FactoryBot syntax.
    # Add:  "config.include FactoryBot::Syntax::Methods" (no quotes) to your spec_helper.rb
    expect(build(:post)).to be_valid
  end
end

And run rspec for this file

$ rspec spec/models/post_spec.rb
Finished in 0.14924 seconds (files took 5.3 seconds to load)
1 example, 0 failures

It worked. Let's add some model validation test now.

# spec/models/post_spec.rb
require 'rails_helper'

RSpec.describe Post, type: :model do
  .....

  let(:post) { create(:post) }

  describe 'ActiveModel validations' do
    it { should validate_presence_of(:title) }
    it { expect(post.published_date).to eq(post.created_at) }
  end
end

And run rspec for this file

$ rspec spec/models/post_spec.rb
...

Finished in 0.22903 seconds (files took 5.81 seconds to load)
3 examples, 0 failures

Next, add specs for scopes

# spec/models/post_spec.rb
require 'rails_helper'

Rspec.describe Post, type: :model do
  ......

  describe 'Scopes' do
    it 'should only include recent posts' do
      create_list(:post, 5, published_date: Time.zone.now - 1.year)
      create_list(:post, 4, published_date: Time.zone.now)

      expect(Post.recent.count).to eq(4)
    end

    it 'should only include past posts' do
      create_list(:post, 5, published_date: Time.zone.now - 1.year)
      create_list(:post, 4, published_date: Time.zone.now)

      expect(Post.past.count).to eq(5)
    end

    it 'should order posts by published_date' do
      post1 = create(:post, published_date: Time.zone.now)
      post2 = create(:post, published_date: Time.zone.now - 2.days)
      post3 = create(:post, published_date: Time.zone.now - 1.day)

      expect(Post.by_published_date).to match_array [post1, post3, post2]
    end

    it 'should only include published posts' do
      create(:post, published_date: Time.zone.now + 2.days, published: true)
      create(:post, published_date: Time.zone.now - 2.days, published: false)
      post = create(:post, published_date: Time.zone.now - 2.days, published: true)

      expect(Post.published).to match_array [post]
    end

    it 'should only include posts published in month & year' do
      create(:post, published_date: Time.zone.now + 2.months, published: true)
      post = create(:post, published_date: Time.zone.now - 2.months, published: true)

      expect(Post.published_in(Time.zone.now - 2.months)).to match_array [post]
    end
  end
end

And run rspec for this file

$ rspec spec/models/post_spec.rb
........

Finished in 0.73464 seconds (files took 8.17 seconds to load)
8 examples, 0 failures

Next, add specs for callbacks

# spec/models/post_spec.rb
require 'rails_helper'

RSpec.describe Post, type: :model do
  .....

  describe 'Callbacks' do
    context 'Slug Sequence' do
      before(:each) do
        @post1 = create(:post, title: 'title')
        @post2 = create(:post, title: 'title')
      end

      it 'should no have the same slug' do
        expect(@post1.slug).not_to eq(@post2.slug)
      end

      it 'should add suffix to slug if duplicates' do
        expect(@post2.slug).to eq("#{@post1.slug}-2")
      end
    end
  end
end

And run rspec for this file

$ rspec spec/models/post_spec.rb
..........

Finished in 0.75823 seconds (files took 5.85 seconds to load)
10 examples, 0 failures

Now let's add tests for faq

# spec/factories/faq.rb
FactoryBot.define do
  factory :faq do
    question { Faker::Lorem.sentence(1) }
    answer   { Faker::Lorem.paragraph(3) }
    position { 0 }
  end
end
# spec/models/faq_spec.rb
require 'rails_helper'

RSpec.describe Faq, type: :model do
  it 'has a valid factory' do
    # Using the shortened version of FactoryBot syntax.
    # Add:  "config.include FactoryBot::Syntax::Methods" (no quotes) to your spec_helper.rb
    expect(build(:faq)).to be_valid
  end

  let(:faq) { create(:faq) }

  describe 'Validations' do
    # Basic validations
    it { should validate_presence_of(:question) }
    it { should validate_presence_of(:answer) }
    it { should validate_presence_of(:position) }
  end

  describe 'Scopes' do
    it 'should order by position' do
      create_list(:faq, 5)
      expect(Faq.by_position.pluck(:position)).to match_array((1..5).to_a)
    end
  end
end

Run the rspec

$ rspec spec/models/faq_spec.rb
.....

Finished in 0.14014 seconds (files took 5.6 seconds to load)
5 examples, 0 failures

Controller Specs

Controller specs are used to test out instance variables assignment, http status, template rendering.

Run all the specs

$ rspec spec
*****..........

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) FaqsController Add specs for FaqsController
     # Not yet implemented
     # ./spec/controllers/faqs_controller_spec.rb:4

  2) HomeController Add specs for HomeController
     # Not yet implemented
     # ./spec/controllers/home_controller_spec.rb:4

  3) PostsController Add specs for PostsController
     # Not yet implemented
     # ./spec/controllers/posts_controller_spec.rb:4

  4) AdminUser add some examples to (or delete) /Users/ilunglee/Work/rails/mock_project/spec/models/admin_user_spec.rb
     # Not yet implemented
     # ./spec/models/admin_user_spec.rb:4

  5) Contact add some examples to (or delete) /Users/ilunglee/Work/rails/mock_project/spec/models/contact_spec.rb
     # Not yet implemented
     # ./spec/models/contact_spec.rb:4


Finished in 0.2833 seconds (files took 5.34 seconds to load)
15 examples, 0 failures, 5 pending

You can see that we are missing some controller specs

Let's add them for the controllers

# spec/controllers/home_controller_spec.rb
require 'rails_helper'

RSpec.describe HomeController, type: :controller do
  describe 'GET index' do
    it 'should be 200' do
      get :index
      expect(response).to have_http_status(200)
    end

    it 'should limit posts to 3' do
      create_list(:post, 10, published: true)

      get :index
      expect(assigns(:posts).count).to eq(3)
    end

    it 'should order posts by published_date' do
      post1 = create(:post, published: true, published_date: Time.zone.now - 3.hours).decorate
      post2 = create(:post, published: true, published_date: Time.zone.now - 2.hours).decorate
      post3 = create(:post, published: true, published_date: Time.zone.now).decorate

      get :index
      expect(assigns(:posts)).to eq([post3, post2, post1])
    end

    it 'should order faqs by position' do
      faq1 = create(:faq, position: 3).decorate
      faq2 = create(:faq, position: 1).decorate
      faq3 = create(:faq, position: 2).decorate

      get :index
      expect(assigns(:faqs)).to eq([faq2, faq3, faq1])
    end
  end

  describe 'GET pricing' do
    it 'should be 200' do
      get :pricing
      expect(response).to have_http_status(200)
    end
  end
end
# spec/controllers/faqs_controller_spec.rb
require 'rails_helper'

RSpec.describe FaqsController, type: :controller do
  describe 'GET index' do
    it 'should be 200' do
      get :index
      expect(response).to have_http_status(200)
    end

    it 'should order faqs by position' do
      faq1 = create(:faq, position: 3).decorate
      faq2 = create(:faq, position: 1).decorate
      faq3 = create(:faq, position: 2).decorate

      get :index
      expect(assigns(:faqs)).to eq([faq2, faq3, faq1])
    end
  end
end

Run spec for this file

$ rspec spec/controllers/faqs_controller_spec.rb
..

Finished in 0.17615 seconds (files took 6.02 seconds to load)
2 examples, 0 failures
# pec/controllers/posts_controller_spec.rb
require 'rails_helper'

RSpec.describe PostsController, type: :controller do
  describe 'GET index' do
    it 'should be 200' do
      get :index
      expect(response).to have_http_status(200)
    end

    it 'should filter posts by month & year' do
      create(:post, published: true, published_date: Time.zone.now).decorate
      create(:post, published: true, published_date: Time.zone.now - 1.month).decorate
      create(:post, published: true, published_date: Time.zone.now - 2.months).decorate

      get :index
      expect(assigns(:posts).count).to eq(1)
    end

    it 'should filter posts by category' do
      create(:post, published: true, category_list: 'tag_1').decorate
      create(:post, published: true, category_list: 'tag_2').decorate

      get :index, params: { category: 'tag_1' }
      expect(assigns(:posts).count).to eq(1)
    end

    it 'should order posts by published_date' do
      post1 = create(:post, published: true)
      post2 = create(:post, published: true)
      post3 = create(:post, published: true)

      get :index
      expect(assigns(:posts).pluck(:published_date)).to(
        match_array([post3.published_date, post2.published_date, post1.published_date])
      )
    end

    it 'should not include future posts' do
      create(:post, published: true, published_date: Time.zone.now + 1.day).decorate
      post2 = create(:post, published: true, published_date: Time.zone.now).decorate

      get :index
      expect(assigns(:posts).pluck(:id)).to match_array([post2.id])
    end

    include_examples 'post aside'
  end

  describe 'GET show' do
    let(:post) { create(:post, published: true) }

    it 'should be 200' do
      get :show, params: { id: post.slug }
      expect(response).to have_http_status(200)
    end

    include_examples 'post aside'
  end
end

Run the specs for posts_controller

$ rspec spec/controller/posts_controller_spec.rb
..........

Finished in 0.7067 seconds (files took 5.42 seconds to load)
10 examples, 0 failures

Here we actually have two block of code for index, and show actions that are exactly the same.

it 'should assigns dates' do
  create(:post, published: true, published_date: Time.zone.now).decorate
  create(:post, published: true, published_date: Time.zone.now - 1.month).decorate

  get :index
  expect(assigns(:dates).count).to eq(2)
end

it 'should assigns categories' do
  create(:post, published: true, category_list: %w[tag_1 tag_2]).decorate
  create(:post, published: true, category_list: %w[tag_1]).decorate

  get :index
  expect(assigns(:categories).count).to eq(2)
end

Let's extract this code

Create spec/shared_examples/controller/aside.rb

# spec/shared_examples/controller/aside.rb
RSpec.shared_examples 'post aside' do
  it 'should assigns dates' do
    create(:post, published: true, published_date: Time.zone.now).decorate
    create(:post, published: true, published_date: Time.zone.now - 1.month).decorate

    get :index
    expect(assigns(:dates).count).to eq(2)
  end

  it 'should assigns categories' do
    create(:post, published: true, category_list: %w[tag_1 tag_2]).decorate
    create(:post, published: true, category_list: %w[tag_1]).decorate

    get :index
    expect(assigns(:categories).count).to eq(2)
  end
end

Change spec/controllers/posts_controller_spec.rb to ...

# spec/controllers/posts_controller_spec.rb
require 'rails_helper'

RSpec.describe PostsController, type: :controller do
  describe 'GET index' do
    .....

    include_examples 'post aside'
  end

  describe 'GET show' do
    .....

    include_examples 'post aside'
  end
end

Run spec again

$ rspec spec/controllers/posts_controller_spec.rb
..........

Finished in 0.71299 seconds (files took 7.55 seconds to load)
10 examples, 0 failures

Great the shared_examples work as expected.

Decorator Specs

Decorator specs are used to test display formatting.

Create files

# spec/decorators/post_decorator_spec.rb
require 'rails_helper'

describe PostDecorator do
  let(:content) { Faker::Lorem.characters(300) }

  let(:post) do
    create(:post, published_date: Time.zone.now, content: "<p>#{content}</p>").
      decorate
  end

  it 'returns published_date in %B %d, %Y format' do
    expect(post.published_date).to eq(Time.zone.now.strftime('%B %d, %Y'))
  end

  it 'returns published_date_short in %b %d, %Y format' do
    expect(post.published_date_short).to eq(Time.zone.now.strftime('%b %d, %Y'))
  end

  it 'returns content_short without html and truncate to 250 letters' do
    expect(post.content_short).to eq("#{content[0..246]}...")
  end
end

Run specs for this file

$ rspec spec/decorators/post_decorator_spec.rb
...
Finished in 0.2513 seconds (files took 5.96 seconds to load)
3 examples, 0 failures

Go to spec/decorators/faq_decorator_spec.rb. Since we don't have any decorator methods for Faq yet we will add a pending message just to remind ourselves.

# spec/decorators/faq_decorator_spec.rb
require 'rails_helper'

describe FaqDecorator do
  pending "add some examples to (or delete) #{__FILE__}"
end

Now run specs again

$ rspec spec
Pending: (Failures listed here are expected and do not affect your suite's status)

  1) FaqDecorator add some examples to (or delete) /Users/ilunglee/Work/rails/mock_project/spec/decorators/faq_decorator_spec.rb
     # Not yet implemented
     # ./spec/decorators/faq_decorator_spec.rb:4

  2) AdminUser add some examples to (or delete) /Users/ilunglee/Work/rails/mock_project/spec/models/admin_user_spec.rb
     # Not yet implemented
     # ./spec/models/admin_user_spec.rb:4

  3) Contact add some examples to (or delete) /Users/ilunglee/Work/rails/mock_project/spec/models/contact_spec.rb
     # Not yet implemented
     # ./spec/models/contact_spec.rb:4

We now have 3 pending specs including the one we just added.

Feature Specs

Feature specs are used to mimic browser interactions by website visiters. It can test status, css, html

Let's add some scenarios for home_controller. Create a file spec/features/home_controller_spec.rb

# spec/features/home_controller_spec.rb
require 'rails_helper'

RSpec.describe HomeController, type: :feature do
  feature 'As a visitor I want to see home page' do
    scenario 'it should respond with success' do
      visit root_path
      expect(page).to have_http_status(:success)
    end

    scenario 'it should have welcome msg' do
      visit root_path
      expect(page).to have_css 'h2', text: 'WELCOME!'
    end
  end

  feature 'As a visitor I want to see pricings' do
    scenario 'it should respond with success' do
      visit pricing_path
      expect(page).to have_http_status(:success)
    end
  end
end

Run rspec for this file

$ rspec spec/features/home_controller_spec.rb
...

Finished in 1.61 seconds (files took 5.8 seconds to load)
3 examples, 0 failures

Great! Let's move on to add feature specs for the rest of the pages.

# spec/features/faqs_controller_spec.rb
require 'rails_helper'

RSpec.describe FaqsController, type: :feature do
  feature 'As a visitor I want to see faqs' do
    scenario 'it should respond with success' do
      visit faqs_path
      expect(page).to have_http_status(:success)
    end
  end
end
# spec/features/posts_controller_spec.rb
require 'rails_helper'

RSpec.describe PostsController, type: :feature do
  feature 'As a visitor I want to see posts' do
    scenario 'it should respond with success' do
      visit posts_path
      expect(page).to have_http_status(:success)
    end
  end

  feature 'As a visitor I want to see individual post' do
    scenario 'it should respond with success' do
      post = create(:post, published: true)
      visit post_path(post)
      expect(page).to have_http_status(:success)
    end
  end
end

We see here that we have a lot of repetitive codes, and most of our feature specs are testing success response(HTTP 200 OK) only. Let's extract the duplicate codes to make the tests cleaner.

Let's create a shared example rspec/shared_examples/features/crud.rb p.s.CRUD stands for Create, Read, Update, Delete which are standard http actions. Here we only have index and show but that's fine we can always add more later on.

# rspec/shared_examples/features/crud.rb
RSpec.shared_examples 'features/controller index' do
  scenario 'view index' do
    visit index_path
    expect(page).to have_http_status(:success)
  end
end

RSpec.shared_examples 'features/controller show' do
  scenario 'view show' do
    visit show_path
    expect(page).to have_http_status(:success)
  end
end

Next let's clean up the feature specs

# spec/features/home_controller_spec.rb
require 'rails_helper'

RSpec.describe HomeController, type: :feature do
  feature 'As a visitor I want to see home page' do
    let(:index_path) { root_path }

    it_behaves_like 'features/controller index'

    scenario 'it should have welcome msg' do
      visit index_path
      expect(page).to have_css 'h2', text: 'WELCOME!'
    end
  end

  feature 'As a visitor I want to see pricings' do
    let(:index_path) { pricing_path }

    it_behaves_like 'features/controller index'
  end
end
# spec/features/faqs_controller_spec.rb
require 'rails_helper'

RSpec.describe FaqsController, type: :feature do
  let(:index_path) { faqs_path }

  feature 'As a visitor I want to see faqs' do
    it_behaves_like 'features/controller index'
  end
end
# spec/features/posts_controller_spec.rb
require 'rails_helper'

RSpec.describe PostsController, type: :feature do
  let(:post)       { create(:post, published: true) }
  let(:index_path) { posts_path }
  let(:show_path)  { post_path(post) }

  it_behaves_like 'features/controller index'
  it_behaves_like 'features/controller show'
end

Let's run rspec and see if it works

$ rspec spec
......

Finished in 1.86 seconds (files took 6.51 seconds to load)
6 examples, 0 failures

Great it works! Let's commit your code and move on...

$ git add -A
$ git commit -m 'Home, Post & Faq Specs'

↪️ Next Section: Form Helper