Michael Anhari

Test-Driven Development (TDD) with Rails 5: Integration Testing

Open laptop in front of a window.

Today we're going to cover integration tests in Rails 5 and pick up where we left off after building the core feature of our blog using TDD and the feedback from a system test.

Our system test used our application from a user's perspective by running our code in a headless browser.

The integration test is the next step inward into our outside-in testing approach, and we'll use it to test how pieces of our application (primarily the router and controllers) are working together.

Our current test coverage

An integration test can test several concepts, such as a user flowing through the application or the responsibilities of a controller. In our case, we're going to test the logic handled by our controller that isn't really covered by our system test.

Let's take a look at our system test from the first post in this series:

# test/system/user_creates_an_article_test.rb

require "application_system_test_case"

class UserCreatesAnArticleTest < ApplicationSystemTestCase
  test "user creates an article" do
    visit articles_path
    click_on "New Article"
    fill_in "Title", with: "Best article ever"
    fill_in "Body", with: "Best article ever. No seriously."
    click_on "Create Article"

    assert_text "Best article ever"
    assert_text "Best article ever. No seriously."
  end
end

System tests should really only assert behavior exposed to the end-user of your application, which is what we are doing here. We're asserting that the article's title and body are shown on the page after we fill out and submit the form. But I can think of a few things that we are not testing directly, but could be if we spin up an integration test for our controller:

  • We are redirecting to the article's show page.
  • We are creating a new record in our database.

A Quick Note.

Controller tests have been a bit of hot-button issue amongst rails developers. Some argue that they're not necessary since system tests likely test the same behavior indirectly, which is largely true. In fact, the first two controller tests that we're going to write will already be in a passing state. It comes down to what is important to you.

If you want to test the specifics and are okay with a bit of testing overlap, then integration tests are there for you to reach for.

Spinning up our integration test

Let's use a rails generator to create a shell of our integration test.

$ bin/rails generate test_unit:controller articles
# tests/controlelrs/articles_controller_test.rb

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end
end

Writing our first integration test

Let's replace the commented example test with some code that will test that we are creating an Article in our database:

# test/controllers/articles_controller_test.rb

require 'test_helper'

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should create an article" do
    assert_difference('Article.count') do
      post articles_url,
        params: {
          article: {
            body: 'Rails is awesome!',
            title: 'Hello Rails',
          },
        }
    end
  end
end

The assert_difference assertion will test the numeric difference between the return value of an expression as a result of what is evaluated in the yielded block. In our test, we are comparing the value of Article.count before and after we execute the body of our block - making a POST request to articles_url which will be routed to our controller's create action.

If we run our test, it passes:

$ bin/rails test test/controllers/articles_controller_test.rb
Run options: --seed 39218

# Running:

.

Finished in 0.102167s, 9.7879 runs/s, 9.7879 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Writing our second test

Let's test the target of our redirect after an article is created in our controller:

# test/controllers/articles_controller_test.rb

require 'test_helper'

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should create an article" do
    assert_difference('Article.count') do
      post articles_url,
        params: {
          article: {
            body: 'Rails is awesome!',
            title: 'Hello Rails',
          },
        }
    end
  end

  test "should redirect to the article" do
    post articles_url,
      params: {
        article: {
          body: 'Rails is awesome!',
          title: 'Hello Rails',
        },
      }

    assert_redirected_to article_path(Article.last)
  end
end

If we run our integration test, both tests should pass:

$ bin/rails test test/controllers/articles_controller_test.rb
Run options: --seed 1444

# Running:

..

Finished in 0.111943s, 17.8662 runs/s, 26.7994 assertions/s.
2 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Testing the setting of a flash message

Let's add our first failing integration test that will drive some behavior by our application. It would be nice if there was a flash message that told the user they created an article successfully.

# test/controllers/articles_controller_test.rb

require 'test_helper'

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should create an article" do
    assert_difference('Article.count') do
      post articles_url,
        params: {
          article: {
            body: 'Rails is awesome!',
            title: 'Hello Rails',
          },
        }
    end
  end

  test "should redirect to the article" do
    post articles_url,
      params: {
        article: {
          body: 'Rails is awesome!',
          title: 'Hello Rails',
        },
      }

    assert_redirected_to article_path(Article.last)
  end

  test "should set a flash message" do
    post articles_url,
      params: {
        article: {
          body: 'Rails is awesome!',
          title: 'Hello Rails',
        },
      }

    assert_equal 'Article was successfully created.', flash[:notice]
  end
end

This time around, let's narrow our test runs to only execute our new test. We can do this by passing the --name flag and the name of the test method to execute to our test command.

$ bin/rails test test/controllers/articles_controller_test.rb --name test_should_set_a_flash_message

You may be asking "Where did test_should_set_a_flash_message come from?" As it turns out, our test blocks are merely some syntactic sugar defined by some metaprogramming the Rails team has in place.

# A method with a name formed by replacing the spaces in the string with
# underscores and prefixing it with `test_` is defined, and the `do`/`end`
# block becomes the method's internals.

test "the truth" do
  assert true
end

# becomes...

def test_the_truth
  assert true
end

You can actually write your tests by defining test_* methods and skip this processing step if you wish. Some developers prefer that approach since it stays truer to object-oriented design.

Source: the implementation of ActiveSupport::Testing::Declarative.

If we run our new test, we'll see our first failure:

$ bin/rails test test/controllers/articles_controller_test.rb --name test_should_set_a_flash_message

Failure:
ArticlesControllerTest#test_should_set_a_flash_message:
--- expected
+++ actual
@@ -1 +1 @@
-"Article was successfully created."
+nil

Let's set a flash message and rerun our test:

# app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def index; end

  def new
    @article = Article.new
  end

  def create
    @article = Article.create(article_params)
    flash[:notice] = "Article was successfully created."
    redirect_to article_path(@article)
  end

  def show
    @article = Article.find(params[:id])
  end

  private

  def article_params
    params.require(:article).permit(:title, :body)
  end
end
$ bin/rails test test/controllers/articles_controller_test.rb --name test_should_set_a_flash_message

.

Finished in 0.102800s, 9.7276 runs/s, 9.7276 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

One little problem

If you try creating a new article in the browser, no flash message is going to show up. What gives?

Remember that we are only testing that the flash message gets set. We are not testing the flash message from the user's point of view.

To get your flash messages to display in your browser you can replace your application layout with the following code:

<!-- app/views/layouts/application.html.erb -->

<!DOCTYPE html>
<html>
  <head>
    <title>Blog</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <% flash.each do |name, msg| %>
      <%= content_tag :div, msg, class: name %>
    <% end %>
    <%= yield %>
  </body>
</html>

Wrap up

In the next post of this series, we'll require an article to have a title and body (if you submit the form with empty values it will currently work). This feature will allow us to drill into model testing - the center of our testing sphere. We'll also refactor our controller to handle unhappy paths like when saving an article fails.

Cheers friend.

Newsletter

I'm working on sending out a weekly newsletter. I'll make it as easy as possible to unsubscribe at any time.