RSpec Rails Controller Test
Ruby on Rails is one of the most popular frameworks for developing web applications. Learn how to make sure your code is good to go with RSpec.
Join the DZone community and get the full member experience.
Join For FreeRails is a web development framework, where model, view, and controller are important aspects of your application. Controllers, just like models and viewers, need to be tested with the Ruby community's favorite tool, RSpec.
Controllers in Rails accept HTTP requests as their input and deliver back and HTTP response as an output.
Organizing Tests
Describe and context blocks are crucial for keeping tests organized into a clean hierarchy, based on a controller's actions and the context we're testing. Betterspecs.org provides the basics about writing your tests, it will help you make your tests much more expressive.
'The purpose of 'describe' is to wrap a set of tests against one functionality while 'context' is to wrap a set of tests against one functionality under the same state.' - Describe vs. Context in RSpec by Ming Liu
You want to create a context for each meaningful input and wrap it into a describe block.
We will express each HTTP session in different describe blocks for: stories_controller_spec.rb
.
describe "Stories" do
describe "GET stories#index" do
context "when the user is an admin" do
it "should list titles of all stories"
end
context "when the user is not an admin" do
it "should list titles of users own stories" do
end
When you want to control the authorization access you can create a new context for each user role. In the same way, you can manage the authentication access, by creating a new context for logged in and logged out users.
context "when the user is logged in" do
it "should render stories#index"
end
context "when the user is logged out" do
it "should redirect to the login page"
end
end
By default, RSpec-Rails configuration disables rendering of templates for controller specs. You can enable it by adding render_views
:
- Globally, by adding it to
RSpec.configure
block inrails_helper.rb
file - Per individual group
describe "GET stories#show" do
it "should render stories#show template" do
end
end
describe "GET stories#new" do
it "should render stories#new template" do
end
end
It is very common to check if you are using valid or invalid attributes before saving them to the database.
describe "POST stories#create" do
context "with valid attributes" do
it "should save the new story in the database"
it "should redirect to the stories#index page"
end
context "with invalid attributes" do
it "should not save the new story in the database"
it "should render stories#new template"
end
end
end
How to Get Your Data Ready?
We use factories to get the data ready for our controller specs. The way factories work can be improved with a FactoryGirl gem.
With the following factory we will generate multiple stories by using a sequence
of different titles and contents:
FactoryGirl.define do
factory :story do
user
sequence(:title) { |n| "Title#{n}" }
sequence(:content) { |n| "Content#{n}" }
end
end
Let's Test This Out!
The time has come to create our own controller tests. The tests are written using RSpec and Capybara. We will cover stories_controller.rb
with tests for each of these methods:
#index
First, we want to take a look at our controller stories_controller.rb
. The index action authorizes access to stories depending if the current user is an admin:
def index
@stories = Story.view_premissions(current_user).
end
And in model story.rb
we check if the current user is an admin:
def self.view_premissions(current_user)
current_user.role.admin? ? Story.all : current_user.stories
end
With the info we just gathered, we can create the following GET stories#index test:
describe "GET stories#index" do
context "when the user is an admin" do
it "should list titles of all stories" do
admin = create(:admin)
stories = create_list(:story, 10, user: admin)
login_as(admin, scope: :user)
visit stories_path
stories.each do |story|
page.should have_content(story.title)
end
end
end
context "when the user is not an admin" do
it "should list titles of users own stories" do
user = create(:user)
stories = create_list(:story, 10, user: user)
login_as(user, scope: :user)
visit stories_path
stories.each do |story|
page.should have_content(story.title)
end
end
end
end
As you can see, we created two different contexts for each user role (admin and not admin). The admin user will be able to see all the story titles, on the other hand, standard users can only see their own.
Using options create(:user)
and create_list(:story, 10, user: user)
you can create users and ten different stories for that user. The newly created user will login login_as(user, scope: :user)
and visit the stories_path
page, where he can see all the story titles depending on his current role page.should have_content(story.title)
.
Another great way to create new users is using let or before blocks, those are two different ways to write DRY tests.
#show
You can write the #show method tests in a similar way. The only difference is that you want to access the page that shows the story you want to read.
describe "GET stories#show" do
it "should render stories#show template" do
user = create(:user)
story = create(:story, user: user)
login_as(user, scope: :user)
visit story_path(story.id)
page.should have_content(story.title)
page.should have_content(story.content)
end
end
Once again we want to create the user create(:user)
and a story create(:story, user: user)
. The created user will log in and visit the page that contains the story based on the story.id visit story_path(story.id)
.
#new and #create
Unlike the others, this method creates a new story. Let's check out the following action in stories_controller.rb
# GET stories#new
def new
@story = Story.new
end
# POST stories#create
def create
@story = Story.new(story_params)
if @story.save
redirect_to story_path(@story), success: "Story is successfully created."
else
render action: :new, error: "Error while creating new story"
end
end
private
def story_params
params.require(:story).permit(:title, :content)
end
The new
action renders a stories#new
template, it is a form that you fill out before creating a new story using the create
action. On successful creation, the story will be saved in the database.
describe "POST stories#create" do
it "should create a new story" do
user = create(:user)
login_as(user, scope: :user)
visit new_stories_path
fill_in "story_title", with: "Ruby on Rails"
fill_in "story_content", with: "Text about Ruby on Rails"
expect { click_button "Save" }.to change(Story, :count).by(1)
end
end
This time a created and logged in user will visit the page where it can create a new story visit new_stories_path
. The next step is to fill up the form with title and content fill_in "...", with: "..."
. Once we click on the save button click_button "Save"
, the number of total stories will increase by one change(Story, :count).by(1)
, meaning that the story was successfully created.
#update
Everyone wants to be able to update their stories. This can be easily done in the following way:
def update
if @story.update(story_params)
flash[:success] = "Story #{@story.title} is successfully updated."
redirect_to story_path(@story)
else
flash[:error] = "Error while updating story"
redirect_to story_path(@story)
end
end
private
def story_params
params.require(:story).permit(:title, :content)
end
When a new story is created we will be able to update it, by visiting the stories edit page.
describe "PUT stories#update" do
it "should update an existing story" do
user = create(:user)
login_as(user, scope: :user)
story = create(:story)
visit edit_story_path(story)
fill_in "story_title", with: "React"
fill_in "story_content", with: "Text about React"
click_button "Save"
expect(story.reload.title).to eq "React"
expect(story.content).to eq "Text about React"
end
end
Just like in the previous methods, a newly created logged in user will create a story and visit the edit story page edit_story_path(story)
. Once we update the title and content of the story it is expected to change as we asked expect(story.reload.title).to eq "React"
.
#delete
At last, we want to be able to delete the stories we disliked.
def destroy
authorize @story
if @story.destroy
flash[:success] = "Story #{@story.title} removed successfully"
redirect_to stories_path
else
flash[:error] = "Error while removing story!"
redirect_to story_path(@story)
end
end
You want to make it sure that only the admin and owner of the story can delete it, by installing gem 'pundit'
.
class StoryPolicy < ApplicationPolicy
def destroy?
@user.role.admin?
end
end
Let's test this out as well.
describe "DELETE stories#destroy" do
it "should delete a story" do
user = create(:admin)
story = create(:story, user: user)
login_as(user, scope: :user)
visit story_path(story.id)
page.should have_link("Delete")
expect { click_link "Delete" }.to change(Story, :count).by(-1)
end
end
The test is written in a similar way to stories#create
, with a major difference. Instead of creating the story, we delete it and thus reduce the overall count by one change(Story, :count).by(-1)
.
Published at DZone with permission of Kristina Garcia Francisco, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments