Test-Driven Development (TDD) with Rails 5: Integration Testing
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.