Learn Ruby on Rails Book

Controller tests

Writing functional controller tests

In previous chapters, we have seen the testing setup and testing of models. Similarly, we should test controllers. In Rails, testing the various actions of a controller is a form of writing functional tests.

Controllers handle the incoming web requests to your application and eventually respond with data or a rendered view. You should test how your actions handle the requests and what kind of responses they send back.

The following are some outlines for the kind of controller test cases that you can write:

  • Was the web request successful?
  • Was the user successfully authenticated?
  • Was the user redirected to the right page?
  • Was the appropriate message displayed to the user in the view?
  • Was the correct information displayed in the response?

Remember we write controller tests inside test/controllers/ directory. Create a home_controller_test.rb file, if it doesn't exist:

1touch test/controllers/home_controller_test.rb

It's a convention to use the _controller_test suffix for all the controller test files.

The test file name will be the snake_case version of the corresponding class name.

Now add the following code inside home_controller_test.rb.

1require 'test_helper'
2
3class HomeControllerTest < ActionDispatch::IntegrationTest
4  def test_should_get_successfully_from_root_url
5    get root_url
6    assert_response :success
7  end
8end

The HomeControllerTest class has inherited from ActionDispatch::IntegrationTest. Thus all methods, which helps with integration testing, are now available in the HomeControllerTest class too.

By requiring test_helper at the top, we have ensured that the default configurations for testing are made available to this particular test.

In the test_should_get_successfully_from_root_url, Rails simulates a request on the action called index from Home controller. It's making sure the request was successful and the status code returned was :ok.

Now let's run this test and it should pass. In our terminal, we will execute the following command.

1bundle exec rails test -v test/controllers/home_controller_test.rb

Testing Tasks controller

Rails provides us helper methods to mock an HTTP request.

Those HTTP helper methods can accept up to 6 arguments:

  • The URI of the controller action you are requesting. This can be in the form of a string or a route helper (e.g. tasks_url).
  • params: option with a hash of request parameters to pass into the action.
  • headers: for setting the headers that will be passed with the request.
  • env: for customizing the request environment as needed.
  • xhr: whether the request is Ajax request or not. Can be set to true for marking the request as Ajax.
  • as: for encoding the request with different content type.

Let's write tests for our TasksController. Create a tasks_controller_test.rb file, if it doesn't exist:

1touch test/controllers/tasks_controller_test.rb

We will be creating two users: a @creator and an @assignee. The @creator always create the tasks and assigns them either to themselves or to the @assignee. We need these two users to test our authorization logic.

Now add the following code inside tasks_controller_test.rb.

1require 'test_helper'
2
3class TasksControllerTest < ActionDispatch::IntegrationTest
4  def setup
5    @creator = create(:user)
6    @assignee = create(:user)
7    @task = create(:task, user: @assignee, creator_id: @creator.id)
8    @creator_headers = headers(@creator)
9    @assignee_headers = headers(@assignee)
10  end
11
12  def test_should_list_all_tasks_for_valid_user
13    get tasks_url, headers: @creator_headers
14    assert_response :success
15    response_body = response.parsed_body
16    all_tasks = response_body['tasks']
17
18    pending_tasks_count = Task.where(progress: 'pending').count
19    completed_tasks_count = Task.where(progress: 'completed').count
20
21    assert_equal all_tasks['pending'].length, pending_tasks_count
22    assert_equal all_tasks['completed'].length, completed_tasks_count
23  end
24
25  def test_should_create_valid_task
26    post tasks_url, params: { task: { title: 'Learn Ruby', user_id: @creator.id } }, headers: @creator_headers
27    assert_response :success
28    response_json = response.parsed_body
29    assert_equal response_json['notice'], t('successfully_created', entity: 'Task')
30  end
31end

In response.parsed_body the parsed_body method parses the response body using the json encoder.

Remember when we run the application, our axios.js file adds some default headers for each of the HTTP requests made from client.

These headers primarily help with authorizing a user in the backend as part of each request.

Similarly, for the test cases, we can set such headers using the headers function which we have defined in our test_helper.

Since we have enforced authentication and authorization for TasksController, we need to pass the user's email and authentication token in the request headers.

So, we have created objects @creator_headers and @assignee_headers for our users using the headers method.

Let's define the headers method in our test/test_helper.rb. Add the following method into test_helper.rb:

1#previous code
2
3def headers(user, options = {})
4  {
5    'X-Auth-Token' => user.authentication_token,
6    'X-Auth-Email' => user.email
7  }.merge(options)
8end

First test is testing index action which should list all tasks for valid user. test_should_list_all_tasks_for_valid_user test case asserts that the count of both pending and completed tasks received via response for a user is same as the count of items within DB.

Comparing with the count of items in database is a strong indicator of a reliable test case. It accurately makes sure that we are testing the right entity.

test_should_create_valid_task is testing the create action of TasksController. We are testing the response message string using the translation helper method t().

Hope you now understand the benefit of adding strings to en.yml. We don't need to hardcode the strings anymore.

Let's add some more test cases into tasks_controller_test.rb,in order to make sure that our validations as well as other actions are working as intended:

1def test_shouldnt_create_task_without_title
2  post tasks_url, params: { task: { title: '', user_id: @creator.id } }, headers: @creator_headers
3  assert_response :unprocessable_entity
4  response_json = response.parsed_body
5  assert_equal response_json['errors'], "Title can't be blank"
6end
7
8def test_creator_can_update_any_task_fields
9  new_title = "#{@task.title}-(updated)"
10  slug_url = "/tasks/#{@task.slug}"
11  task_params = { task: { title: new_title, user_id: 1 } }
12
13  put slug_url, params: task_params, headers: @creator_headers
14  assert_response :success
15  @task.reload
16  assert_equal @task.title, new_title
17  assert_equal @task.user_id, 1
18end
19
20def test_should_destroy_task
21  initial_task_count = Task.all.size
22  slug_url = "/tasks/#{@task.slug}"
23
24  delete slug_url, headers: @creator_headers
25  assert_response :success
26  assert_equal Task.all.size, initial_task_count - 1
27end

Let us add test cases to make sure that all the authorizations for a task are working as intended:

1def test_assignee_shouldnt_destroy_task
2  slug_url = "/tasks/#{@task.slug}"
3  delete slug_url, headers: @assignee_headers
4  assert_response :forbidden
5  response_json = response.parsed_body
6  assert_equal response_json['error'], t('authorization.denied')
7end
8
9def test_assignee_shouldnt_update_restricted_task_fields
10  new_title = "#{@task.title}-(updated)"
11  slug_url = "/tasks/#{@task.slug}"
12  task_params = { task: { title: new_title, user_id: 1 } }
13
14  assert_no_changes -> { @task.reload.title } do
15    put slug_url, params: task_params, headers: @assignee_headers
16    assert_response :forbidden
17  end
18end
19
20def test_assignee_can_change_status_and_progress_of_task
21  slug_url = "/tasks/#{@task.slug}"
22  task_params = { task: { status: 'starred', progress: 'completed' } }
23
24  put slug_url, params: task_params, headers: @assignee_headers
25  assert_response :success
26  @task.reload
27  assert @task.starred?
28  assert @task.completed?
29end
30
31def test_creator_can_change_status_and_progress_of_task
32  slug_url = "/tasks/#{@task.slug}"
33  task_params = { task: { status: 'starred', progress: 'completed' } }
34
35  put slug_url, params: task_params, headers: @creator_headers
36  assert_response :success
37  @task.reload
38  assert @task.starred?
39  assert @task.completed?
40end

Now you can try running all the test cases and they should pass.

1bundle exec rails test -v test/controllers/tasks_controller_test.rb

Testing Comments controller

Let's write tests for our Comments Controller. Create a comments_controller_test.rb file, if it doesn't exist.

1touch test/controllers/comments_controller_test.rb

Now add the following code inside comments_controller_test.rb:

1require 'test_helper'
2
3class CommentsControllerTest < ActionDispatch::IntegrationTest
4  def setup
5    @task = create(:task)
6    @headers = headers(@task.user)
7  end
8
9  def test_should_create_comment_for_valid_request
10    content = 'Wow!'
11    post comments_url, params: { comment: { content: content, task_id: @task.id } }, headers: @headers
12    assert_response :success
13    assert_equal @task.comments.last.content, content
14  end
15
16  def test_shouldnt_create_comment_without_content
17    post comments_url, params: { comment: { content: '', task_id: @task.id } }, headers: @headers
18    assert_response :unprocessable_entity
19    response_json = response.parsed_body
20    assert_equal response_json['errors'], "Content can't be blank"
21  end
22end

Here, test_should_create_comment_for_valid_request is testing create action of CommentsController. Test asserts that the newly created comment's content is reflected in the database.

test_shouldnt_create_comment_without_content is a negative test case.

Negative test cases allow us to make sure that the actions fail and send appropriate responses for invalid parameters.

In the above test case, we are making sure that the validation from the comments model is working it's magic.

Now you can try running all the test cases and they should pass.

1bundle exec rails test -v test/controllers/comments_controller_test.rb

Testing Users controller

Let's write tests for our Users Controller. Create a users_controller_test.rb file, if it doesn't exist.

1touch test/controllers/users_controller_test.rb

Now add the following code inside users_controller_test.rb.

1require 'test_helper'
2
3class UsersControllerTest < ActionDispatch::IntegrationTest
4  def test_should_list_all_users
5    get users_url
6    assert_response :success
7    response_json = response.parsed_body
8    assert_equal response_json['users'].length, User.count
9  end
10
11  def test_should_signup_user_with_valid_credentials
12    post users_url, params: { user: { name: 'Sam Smith',
13                                      email: 'sam@example.com',
14                                      password: 'welcome',
15                                      password_confirmation: 'welcome' } }
16    assert_response :success
17    response_json = response.parsed_body
18    assert_equal response_json['notice'], t('successfully_created', entity: 'User')
19  end
20
21  def test_shouldnt_signup_user_with_invalid_credentials
22    post users_url, params: { user: { name: 'Sam Smith',
23                                      email: 'sam@example.com',
24                                      password: 'welcome',
25                                      password_confirmation: 'not matching confirmation' } }
26
27    assert_response :unprocessable_entity
28    assert_equal response.parsed_body['errors'], "Password confirmation doesn't match Password"
29  end
30end

Here, above tests validate user signup with credentials.

Now you can try running all the test cases and they should pass.

1bundle exec rails test -v test/controllers/users_controller_test.rb

Testing Sessions controller

Let's write tests for our Sessions Controller. Create a sessions_controller_test.rb file, if it doesn't exist.

1touch test/controllers/sessions_controller_test.rb

Now add the following code inside sessions_controller_test.rb.

1require 'test_helper'
2
3class SessionsControllerTest < ActionDispatch::IntegrationTest
4  def setup
5    @user = create(:user)
6  end
7
8  def test_should_login_user_with_valid_credentials
9    post sessions_url, params: { login: { email: @user.email, password: @user.password } }, as: :json
10    assert_response :success
11    assert response.parsed_body['auth_token']
12  end
13
14  def test_shouldnt_login_user_with_invalid_credentials
15    non_existent_email = 'this_email_does_not_exist_in_db@example.email'
16    post sessions_url, params: { login: { email: non_existent_email, password: 'welcome' } }, as: :json
17
18    assert_response :unauthorized
19    assert_equal response.parsed_body['notice'], 'Incorrect credentials, try again.'
20  end
21end

Here, above tests validate user login with valid credentials and auth_token. If user tries to enter wrong credentials, then user will get 401 i.e. unauthorized response.

Now you can try running all the test cases and they should pass.

1bundle exec rails test -v test/controllers/sessions_controller_test.rb

Now, let's commit the changes:

1git add -A
2git commit -m "Added controller tests"
⌘K
    to navigateEnterto select Escto close
    Previous
    Next