Minitest: Testing Controllers & Requests


Minitest do not come with any feature for testing requests, unlike request spec in RSpec. Fortunately, controller tests are able to test through everything in the backend like request tests if we don't mock anything.

Here's how a controller test works.

A Basic Controller Test

A basic controller test includes a HTTP verb and the response we expect.

require 'minitest/autorun'

class PostsControllerTest < ActionController::TestCase
  def setup
    @post = Post.create(title: 'Hello World')
  end

  def test_index
    get :index
    assert_response 200
    assert_includes @post.title, @response.body
  end
end

From this case we know there are methods and variables accessible exclusively for controller tests like assert_response and @response.

Available Assertions

In controller tests, you got several extra assertions to use:

assert_response :success
assert_response :redirect
assert_response :missing
assert_response :error

assert_redirectd_to "/"
assert_redirectd_to root_path
assert_redirectd_to { controller: 'posts', action: 'index' }

assert_template 'posts/index'
assert_template partial: '_customer', count: 2

Besides to the assertions tailored for controllers, there are instance variables available in the tests:

  • @controller
  • @request
  • @response

I found @response the most useful among them. It is very useful to inspect the @response and see if everything goes well. For example, if we're rendering a JSON output, we can expect there are keys and values contained in the body.

assert_includes @response.body, keys
assert_includes @response.body, values

Testing Actions in Different Files

Request tests can be complicated, and in many cases, a single action can take hundreds of lines to test for its complicated scenerios, including edge cases. It's better to separate complicated test cases into different files.

It is easy to separate model tests because we can name the classes as we want. However, in controller tests, Minitest checks the class name and find the correct controllers for us, which make us a little hard to separate them. Let's suppose we want to separate posts#index and posts#show like this:

- test
  - controllers
    - posts_controller
      - index_test.rb
      - show_test.rb

And the files look like this:

# index_test.rb
require 'test_helper'

class PostsController::IndexTest < ActionController::TestCase
  def test_html
    # Some tests ...
  end

  def test_json
    # Some tests ...
  end
end

and another file:

# show_test.rb
require 'test_helper'

class PostsController::ShowTest < ActionController::TestCase
  def test_respond_200
    # Some tests ...
  end

  def test_without_id
    # Some tests ...
  end
end

In this case, Minitest can identify that these test classes are for specific controllers, so we name the methods with all different cases we want to test.

Testing Routes in Controllers

Testing routes in Minitest is integrated into controller tests under ActionController::TestCase. In this case, we should create another file under the same file structure to test those routes under a controller. It will look something like this:

# test/controllers/posts_controller/test_routes.rb
require 'test_helper'

class PostsController::RoutesTest < ActionController::TestCase
  def test_routes
    assert_routing '/posts',   controller: "posts", action: "index"
    assert_routing '/posts/1', controller: "posts", action: "show", id: "1"
  end
end

Reference