Skip to content

Don't know how to test a Rails controller? This post is for you!

Functional testing in Rails with RSpec and FactoryGirl; what to test in a controller — HTTP statuses, templates, redirects and persistence — covering a full CRUD.

8 min read
  • ruby
  • rails
  • testing

Originally published on Medium

In the previous post I explained what TDD is about and how we test our models in Rails, so this time we'll look at how to write functional tests for our application using the RSpec gem.

Functional tests? (ㆆ_ㆆ). That's right, functional tests. In Rails, testing your controllers is a form of functional testing. I found this out a few weeks ago while looking for examples of functional tests — without realizing it, I was already writing them.

In Rails, unit tests are for your models, while functional tests are for your controllers.

The first thing to keep in mind is: what are we going to test? [it may sound silly, but this goal is often forgotten; tests end up verifying everything except what they were created for (≖_≖ )]. In our controllers we'll test:

The request status. Since our controllers handle the requests reaching our application, we must verify they return the expected status. If a request succeeds, it should have 'status: 200'.

The template or view that should be rendered. Many controller actions have associated views, so we need to make sure the right ones are displayed. For example, the 'new' action should render a view with a form, 'index' a view listing all records, and so on.

Did it redirect to the right page? This may sound like the previous point, but it generally applies to actions that don't render a view; instead, after doing their work, they redirect to a specific page. A clear example is 'create', which after creating a resource (user, article, etc.) does a 'redirect_to' to the resource's URL.

Was the resource saved or updated correctly? We'll verify that the resource was actually persisted to the database or updated correctly. This one applies specifically to 'create' and 'update'.

There are plenty more things to consider — like verifying the user was authenticated correctly, or that the expected message showed up after creating a resource, etc. However, I picked these 4 because I consider them the most important in an application [obviously this can vary depending on the type of app].

Let's look at some code. This will be our 'Users' controller:

class UsersController < ApplicationController
 
  before_action :find_user, only: [:show, :edit, :update, :destroy]
 
  def index
    @users = User.all
  end
 
  def show
    # find_user
  end
 
  def new
    @user = User.new
  end
 
  def create
    @user = User.new(parameters)
    if @user.save
      redirect_to user_path(@user), notice: 'Se ha creado el usuario.'
    else
      render :new
    end
  end
 
  def edit
    # find_user
  end
 
  def update
    if @user.update(parameters)
      redirect_to user_path(@user), notice: 'Se actualizó el usuario.'
    else
      render :edit
    end
  end
 
  def destroy
  # find_user
    redirect_to action: :index, alert: 'Se eliminó el usuario' if @user.destroy
  end
 
  private
 
  def parameters
    params.require(:user).permit(:name, :last_name, :email)
  end
 
  def find_user
    if User.find_by(id: params[:id])
      @user = User.find(params[:id])
    else
      redirect_to users_path, alert: "No se encontró el usuario"
    end
  end
end

Our users_controller has the basic methods for a CRUD (Create, Read, Update, Delete). With all of that in mind, let's look at the tests for it.

A note first: I know you must be wondering why I didn't use TDD (͡° ͜ʖ ͡°) like in the previous posts — the post would have gotten way too long with TDD ( ˘︹˘ ). That said, you should absolutely apply TDD to everything that moves! (≖᷆︵≖). Well, literally everything that moves — you know what I mean.

Alright, on to the tests:

describe UsersController do
 
  describe 'GET index' do
    before do
      @user = FactoryGirl.create(:user)
    end
 
    it 'ok' do
      get :index
      expect(response).to have_http_status(:ok)
      expect(response).to render_template('index')
      expect(assigns(:users)).to eq([@user])
    end
  end
 
  describe 'GET show' do
    it 'ok' do
      get :show, id: @user.id
      expect(response).to have_http_status(:ok)
      expect(response).to render_template('show')
      expect(assigns(:user)).to eq(@user)
    end
 
    it 'when user doesn\'t exist' do
      get :show, id: 69_696
      expect(response).to have_http_status(:redirect)
    end
  end
 
  describe 'GET new' do
    it 'ok' do
      get :new
      expect(assigns(:user)).to be_a_new(User)
    end
  end
 
  describe 'GET edit' do
    it 'ok' do
      get :edit, id: @user.id
      expect(assigns(:user)).to eq(@user)
    end
  end
 
  describe 'POST create' do
    before do
      @attributes = { name: 'Giancarlos', last_name: 'Isasi', email: 'giancarlos@devacademy.la' }
    end
 
    context 'with valid params' do
      it 'creates a new user' do
        User.new
        expect { post :create, user: @attributes }.to change(User, :count).by(1)
      end
 
      it 'assigns a newly created user as @user' do
        post :create, user: @attributes
        expect(assigns(:user)).to be_a(User)
      end
 
      it 'redirects to the created user' do
        post :create, user: @attributes
        expect(response).to redirect_to(user_path(User.last))
      end
    end
  end
 
  describe 'PUT update' do
    before do
      @attributes = { name: 'update Giancarlos' }
    end
 
    context 'with valid params' do
      it 'assigns the user as @user' do
        put :update, id: @user.id, user: @attributes
        expect(assigns(:user).name).to eq @attributes[:name]
      end
 
      it 'redirects' do
        put :update, id: @user.id, user: @attributes
        expect(response).to redirect_to(user_path(@user))
      end
    end
  end
end

Relax, don't panic; it may look like black magic, but it isn't. I'll try to explain each test as best I can (҂◡̀_◡́)ᕤ

First we have a 'describe UsersController' that wraps every test for our controller. The first tests cover the INDEX action, but before anything else we use a 'before' block to create a user before all the tests run.

FactoryGirl.define do
  factory :user do
    name 'Giancarlos'
    last_name 'Isasi'
    email 'giancarlos@devacademy.la'
  end
end

[I created the user using a feature of the FactoryGirl gem; it lets you define 'factories' (containing test data) for all your models and then instantiate them very easily]. So, as I explained at the start, we need to test the request status and the template or view that gets rendered.

describe 'GET index' do
  before do
    @user = FactoryGirl.create(:user)
  end
 
  it 'ok' do
    get :index
    expect(response).to have_http_status(:ok)
    expect(response).to render_template('index')
    expect(assigns(:users)).to eq([@user])
  end
end

Inside our test, aptly named 'ok', we check the status with 'have_http_status(:ok)' [you can use 200 instead of ':ok'; it's the same thing after all], and with 'render_template('index')' [yes, index is the name of our view, in case you hadn't noticed] we state which view should be rendered. Lastly, we verify that our created user (@user) equals the 'users' collection [assigns is a hash, accessible inside Rails tests, containing all the instance variables that would be available to the view at that point].

describe 'GET show' do
  it 'ok' do
    get :show, id: @user.id
    expect(response).to have_http_status(:ok)
    expect(response).to render_template('show')
    expect(assigns(:user)).to eq(@user)
  end
 
  it 'when user doesn\'t exist' do
    get :show, id: 69_696
    expect(response).to have_http_status(:redirect)
  end
end

Now the tests for the SHOW action. We have two tests (lines 18 and 24). The first verifies that when looking up a user we get an http_status of '200' (line 19), that the 'show' template gets rendered (line 20), and finally that our @user equals the 'user' variable at that point [the way assigns works can seem confusing, but it's nothing more than a hash returning the value the variable holds at that point]. The second test (line 24) verifies that when a user doesn't exist, we're automatically redirected to the URL specified in our private 'find_user' method [that method lives in our users_controller; you can check it in the code I posted above]. Also notice that both examples have something like 'get :show, id: @user.id…' — and yes, it's what you think: we're making a 'GET' request to the 'SHOW' action and passing it parameters, in this case the id of the user to look up.

describe 'GET new' do
  it 'ok' do
    get :new
    expect(assigns(:user)).to be_a_new(User)
  end
end

On to the NEW action, which has a single test. It makes a 'GET' request to 'NEW' and then verifies that the 'user' variable was instantiated correctly [in 'NEW' we almost always have something like '@user = User.new', which instantiates the variable used to build the form].

describe 'POST create' do
  before do
    @attributes = { name: 'Giancarlos', last_name: 'Isasi', email: 'giancarlos@devacademy.la' }
  end
 
  context 'with valid params' do
    it 'creates a new user' do
      User.new
      expect { post :create, user: @attributes }.to change(User, :count).by(1)
    end
 
    it 'assigns a newly created user as @user' do
      post :create, user: @attributes
      expect(assigns(:user)).to be_a(User)
    end
 
    it 'redirects to the created user' do
      post :create, user: @attributes
      expect(response).to redirect_to(user_path(User.last))
    end
  end
end

After NEW comes CREATE. First we define (lines 45–47) the parameters for the user we'll create. We create a context called 'with valid params' [to group the tests that run when 'CREATE' receives valid parameters (name isn't 'nil', the email matches the expected format, etc.)]; inside this context we have 3 tests.

'it creates a new user' first instantiates our user variable (no need for @user = User.new; User.new is enough), then does a 'POST' passing a 'user' json with the parameters of the user to create, and expects the number of records in our User model to change by 1.

'assigns a newly created user as @user' does almost the same: we 'POST' with the corresponding parameters and finally expect our 'user' variable, at that point, to have the same structure as our User model [the 'be_a(User)' check verifies our user is a 'User' by checking that the data type of each value in the 'user' variable matches the column types of our User model].

'redirects to the created user' — yes, it's what you think: we do the same 'POST' with parameters, and finally expect to be redirected to that newly created user's page. In case you noticed, we have to pass the last created user as a parameter to 'user_path' [obviously the last user in our User model should match the one we just created].

Then comes EDIT [I didn't follow the order in the test file, since I think 'NEW' and 'POST' belong together, and likewise 'EDIT' and 'UPDATE'].

describe 'GET edit' do
  it 'ok' do
    get :edit, id: @user.id
    expect(assigns(:user)).to eq(@user)
  end
end

So we make another 'GET' request, this time to 'EDIT', passing the id of the user to edit, and then the test expects the 'user' variable (found using the id we passed) to equal our @user variable (created at the beginning).

describe 'PUT update' do
  before do
    @attributes = { name: 'update Giancarlos' }
  end
 
  context 'with valid params' do
    it 'assigns the user as @user' do
      put :update, id: @user.id, user: @attributes
      expect(assigns(:user).name).to eq @attributes[:name]
    end
 
    it 'redirects' do
      put :update, id: @user.id, user: @attributes
      expect(response).to redirect_to(user_path(@user))
    end
  end
end

And finally we test the UPDATE action. In the 'before do…' we specify the data we want to update — in this case, the name. We create a context to group the tests with valid parameters [you must be asking yourself: if there's a 'with valid params', there should be a 'with invalid params', right? And yes, you're right: with invalid parameters we'd basically test that some message or 'alert' shows up saying the user wasn't created or updated, check the 'http_status', etc. — but unfortunately I won't get into that this time].

So we make a 'PUT' request to 'UPDATE'; in both tests (lines 73 and 79) we pass the id of the user to update along with the data to be updated. In the first test we expect 'assigns(:user).name' at that point to equal the 'name' in our @attributes variable, confirming the user was indeed updated with the new 'name' we specified. In the second test, after making the request with its parameters, we expect to be redirected to the URL ('user_path') of the updated user — don't forget to pass our @user object as a parameter.

They didn't use TDD

I hope this post serves as a good introduction to testing with RSpec in Ruby on Rails, and helps you write the tests needed to cover a basic CRUD in a controller. If you have any questions, don't hesitate to drop them in the comments — I'll be glad to help (◡̀_◡́҂). And if you still have no idea what TDD is, I recommend reading my post explaining it (͡° ͜ʖ ͡°), as well as how to apply TDD when building an API with Ruby on Rails (͡° ͜ʖ ͡°).

You can keep reading my other posts:

Thanks for reading; if you liked it, don't forget to recommend and share the post!! (•◡•)\ (•◡•) /

Thanks for reading

Links: