24 Nov 2021 · Software Engineering

    Integration Testing Ruby on Rails with Minitest and Capybara

    11 min read
    Contents

    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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I'm a London based Ruby consultant. Visit my website for more information.