-
Notifications
You must be signed in to change notification settings - Fork 0
Backend Training: Rspec Tests
🔖 : 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.
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 yourGemfile
you are usingfactory_girl_rails
please upgrade it tofactory_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
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 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 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 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'