Introduction
In this tutorial, we’ll cover how to do integration tests in Rails using Minitest and Capybara. We’ll also cover how integration tests can sometimes serve as a replacement for controller tests.
Prerequisites
To follow this tutorial, you’ll need to have Ruby installed along with Rails. This tutorial was tested using Ruby version 2.3.3, Rails version 5.0.0.1, Minitest version 5.10.1, and Capybara version 2.11.1.
Currently, there are no known issues with using earlier or later versions of any of those, however there will be some differences.
To get started, you can use gem install rails
, and you should be good to go.
gem install rails
What is Minitest?
Minitest is a complete testing suite for Ruby, supporting test-driven development (TDD), behavior-driven development (BDD), mocking, and benchmarking. It’s small, fast, and it aims to make tests clean and readable.
If you’re new to Minitest, you can take a look at our tutorial on getting started with Minitest.
Minitest is the default testing suite included by default with new Rails applications, so no further setting up is required to get it to work. Minitest and RSpec are the two most common testing suites used in Ruby. If you’d like to learn more about RSpec, you can read our tutorial on getting started with RSpec as well as this tutorial on mocking with RSpec: doubles and expectations.
What is Capybara?
Capybara is an acceptance test framework for web applications, often used to do end-to-end testing in Rails applications.
It allows developers to simulate a user on a web page, make assertions based on the content and environment of the page, and it also provides an API to interact with the web page.
By default, it will run in headless mode, but it can also use a browser or a number of other drivers instead.
What is Integration Testing?
While unit tests make sure that individual parts of your application work, integration tests are used to test that different parts of your application work together. They are normally used to test at least the most important workflows of applications.
Terminology
Although we refer to this kind of testing as integration testing, it might also be referred to as acceptance testing. The authors of Capybara refer to it as an acceptance testing framework. Sometimes, these kind of tests are also called end-to-end tests.
Integration Tests vs. Controller Tests
The default method for testing controllers in Rails is to use functional tests. However, since controllers are ideally very lean in Rails, and our integration tests are exercising all code paths, they can be omitted in favor of thorough integration tests.
For example, an integration test for a user signup can assert that the welcome email was sent out just as well as a functional controller test can.
Some people prefer to test controllers in isolation. Sometimes, there is a good reason to do this, if your controllers are very complex.
Setup
Provided you have installed Ruby and Rails, you can set up a new Rails application.
rails new integration-testing-minitest
We’ll need to add Capybara to our Gemfile
in the group test. This can be done at the bottom of the file. Note that we’re using a gem called minitest-rails-capybara, which integrates Capybara with Minitest and Rails.
# Gemfile
...
group :test do
gem 'minitest-rails-capybara'
end
We’ll also need to load Capybara in order to use it in our tests.
# test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require "minitest/rails/capybara"
...
We’re now all set to start writing our example application.
Example Application
Now that we have everything set up, we’ll introduce an example application where we make use of integration tests.
Our example application is a blog where we’ll have an index of posts and a link to a page for writing new posts. Each post has a title and a body, and every time a new post is created, an email is sent out to an admin to notify them about the new post.
Since we’re focusing on integration testing, we’ll use generators where we can, and avoid focusing much on other kinds of tests in order to keep this tutorial brief.
Let’s start by running the scaffold generator to create posts.
rails generate scaffold Post title:string body:text
We’ve omitted the output here, it should create all the necessary files for posts. Next we need to migrate the database.
rake db:migrate
== 20161214213527 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0020s
== 20161214213527 CreatePosts: migrated (0.0024s) =============================
The generator created some tests for us, and although we are not going to be using those, it’s a good idea to run them to make sure everything works.
rake
# Running:
.......
Finished in 1.103736s, 6.3421 runs/s, 8.1541 assertions/s.
7 runs, 9 assertions, 0 failures, 0 errors, 0 skips
You can also issue rails server
at the command line and navigate to http://localhost:3000/posts to check the result. We’re going to be using integration tests to automatically test all of this.
Integration Tests
Let’s create our first integration test. We want to navigate to the index and make sure it lists all the blog posts in our database. Let’s start by updating our fixtures so we can write a test for this.
# test/fixtures/posts.yml
one:
title: Post Title One
body: Post body one.
two:
title: Post Title Two
body: Post body two.
Here’s our first integration test.
# test/integration/post_flow_test.rb
require 'test_helper'
class PostFlowTest < Capybara::Rails::TestCase
def setup
@one = posts :one
@two = posts :two
end
test 'post index' do
visit posts_path
assert page.has_content?(@one.title)
assert page.has_content?(@two.title)
end
end
All we did here was visit the post index and make sure that the titles for both posts in our fixtures are present. Let’s run all of our tests again to make sure they all pass. This test should pass without us having to do anything, since the scaffold we generated earlier does this already.
rake
# Running:
........
Finished in 0.515325s, 15.5242 runs/s, 21.3458 assertions/s.
8 runs, 11 assertions, 0 failures, 0 errors, 0 skips
Let move on to something a bit more complicated, and test that we can write a new post and submit it. Place it below the other test in our integration test for posts.
# test/integration/post_flow_test.rb
...
test 'writing a new post' do
visit posts_path
click_on 'New Post'
fill_in 'Title', with: 'Test Title'
fill_in 'Body', with: 'Test Body'
click_on 'Create Post'
assert_current_path post_path(Post.last)
assert page.has_content?('Test Title')
assert page.has_content?('Test Body')
end
end
In this test, we navigate to the post index, click the link to write a new post, fill in the form with some text, and submit the post. We make sure that we’re redirected to the right page and that it contains the text we wrote in the post.
Run the tests again to make sure everything passes. Again, we don’t need to write any code since this test will already pass because of the code generated by the scaffold.
rake
# Running:
.........
Finished in 0.551475s, 16.3199 runs/s, 23.5731 assertions/s.
9 runs, 13 assertions, 0 failures, 0 errors, 0 skips
Now, we have one last feature to add, the email alert to an admin email once a post has been submitted. Let’s start by adding a new test for writing a new post and checking if an admin notice email was sent.
# test/integration/post_flow_test.rb
require 'test_helper'
class PostFlowTest < Capybara::Rails::TestCase
include ActiveJob::TestHelper
def setup
@one = posts :one
@two = posts :two
end
test 'post index' do
visit posts_path
assert page.has_content?(@one.title)
assert page.has_content?(@two.title)
end
test 'writing a new post' do
write_new_post
latest_post = Post.last
assert_current_path post_path(latest_post)
assert page.has_content?('Test Title')
assert page.has_content?('Test Body')
end
test 'writing a new post sends admin notice' do
perform_enqueued_jobs do
write_new_post
last_post = Post.last
mail = ActionMailer::Base.deliveries.last
assert_equal 'admin@example.com', mail['to'].to_s
assert_equal 'New post added', mail.subject
end
end
private
def write_new_post
visit posts_path
click_on 'New Post'
fill_in 'Title', with: 'Test Title'
fill_in 'Body', with: 'Test Body'
click_on 'Create Post'
end
end
Quite a few things changed in this file. At the top, we include ActiveJob::TestHelper
so we can tell ActiveJob
to perform_enqueued_jobs
before running our tests. This is necessary since we plan on using ActiveJob to deliver our email later, instead of doing so in the client request.
We also refactor writing a new post into a private method to keep the tests for writing a new post and for ensuring that the admin email gets sent separate. When it comes to integration tests, this is optional, you could also keep it all in the same test, and split it into methods.
Finally, we get the last email sent by ActionMailer
and make sure it has the correct recipient and subject. We don’t have to go further than that, since the content of the mail should be tested in the test for the mailer itself. This is to avoid testing the same thing in many places, and make our tests less rigid and easier to update.
Now, if you try running running our new tests, they’ll fail since we haven’t added the notification email yet. We’ll be adding a mailer and calling it from PostsController#create
and testing in our integration test instead of writing a separate functional test for the controller to do it.
Let’s create the mailer first:
rails generate mailer PostMailer
This should set up the mailer. Now we need to add our admin notice email to it:
# app/mailers/post_mailer.rb
class PostMailer < ApplicationMailer
def admin_notice(post)
@post = post
mail to: 'admin@example.com', subject: 'New post added'
end
end
We also need the corresponding views:
<%# app/views/post_mailer/admin_notice.html.erb %>
<p>A new post has been added! Here's the post:</p>
<p><%= @post.title %></p>
<%= simple_format @post.body %>
<%# app/views/post_mailer/admin_notice.text.erb %>
A new post has been added! Here's the post:
Title: <%= @post.title %>
<%= @post.body %>
We’ll skip the tests for this mailer to keep this tutorial from getting too long. All we have to do now is call the mailer from the controller after a post has been created.
# app/controllers/posts_controller.rb
...
# POST /posts
# POST /posts.json
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
PostMailer.admin_notice(@post).deliver_later
format.html { redirect_to @post, notice: 'Post was successfully created.' }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
...
We added only one line there to call the mailer. Now, let’s run our tests again and see if they pass.
rake
# Running:
..........
Finished in 0.975611s, 10.2500 runs/s, 15.3750 assertions/s.
10 runs, 15 assertions, 0 failures, 0 errors, 0 skips
All the tests should pass, and we now have an application that is integration tested end-to-end with Minitest and Capybara.
Conclusion
In this tutorial, we covered how to integration test Rails applications with Minitest and Capybara. We now have a functional Rails application that is covered with integration tests. After reading this tutorial, you should have an idea of how integration tests can be used to simulate a user within your Rails application, as well as how integration tests can in part, or fully, replace functional controller tests.
If you found this tutorial helpful, you might want to check out some of the other tutorials on testing in the Semaphore Community.
If you’d like to learn how to set up a BDD stack on a Rails 5 application, this tutorial is the best next read for you.
We’d also like to hear your comments and questions, so feel free to leave them in the section below.
P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.