Implement Rails-like Integration Tests in Ruby

How to Setup Rails-like Integration Tests in Ruby

In this next post in the Rails Companion series, we'll add support for integration testing to our Ruby web application. Specifically, we'll set up the Minitest framework and simulate HTTP requests using the rack-test gem. Finally, we'll add some syntactic sugar to make our tests just like Rails.

9 min read

As a Ruby on Rails developer, you must be well familiar with integration testing, where you make a simulated HTTP request to your application, passing headers and params, and assert the response status and body. Something like this:

test "can create an article" do
  get "/articles/new"
  assert_response :success

  post "/articles", params: { article: { title: "can create", body: "article successfully." } }
  assert_response :redirect
  follow_redirect!
  assert_response :success
  assert_select "p", "Title:\n  can create"
end

Have you ever wondered how does it all work? For example, where do the get, post methods come from, what exactly do they do? How do they make the HTTP request to your application and get the response?

Well, I had the same questions, and to answer them, we are going to be building integration testing in our Rails Companion project. Not only will it teach us how testing works in Rails, but we'll have a solid testing support for our plain, Ruby based web application as we build more Rails features in future.

This is the third post in my course Rails Companion, where we build a complete web application using Ruby, without Rails. The goal is to understand how web applications work, and to develop a deeper understanding and appreciation for everything Rails does for us.

To learn more, check out this post: Announcing Rails Companion 💡, and you can purchase the pre-release version of the course here 🙏

So far, this is our simple web application written in Ruby.

require_relative './config/routes'

class App
  def call(env)
    headers = { 'Content-Type' => 'text/html' }

    response_html = router.build_response(env)

    [200, headers, [response_html]]
  end

  private
    def router
      Router.instance
    end
end

In the previous post, we added simple routing support to it. Here is our routes file containing three routes.

require_relative '../router'

Router.draw do
  get('/') { "Hello World" }

  get('/articles') { 'All Articles' }

  get('/articles/1') do |env|
    puts "Path: #{env['PATH_INFO']}"
    "First Article"
  end
end
Build Your Own Router in Ruby
Have you always wondered what a Router is and how it works? I know I have. In this second bonus post in my Rails Companion course, we’ll build our own router in Ruby to get a deeper understanding of the Rails Router. We’ll also use some meta-programming to make it look just like the Rails router.

So far, whenever we made a change, we had to open the browser, enter the URL, and ensure we got the expected response back. It worked, but it is tedious and also, not practical. You are simply not going to visit each and every route to ensure it works as expected.

Testing provides a faster and efficient way to automate this activity.

Our goal is to write unit tests to ensure that when we visit the web application on the above routes, we receive the expected response.

Let's get started.

Step 1: Install a Testing Framework

I am a big fan of Minitest and how simple it is. The entire framework code consists of 18 files and 2230 lines of code! It's written in plain Ruby and you can just read it to understand how it works. It's easy, fast, readable, simple, and clean!

All this to say, we will be using Minitest to test our web application. Let's install it using bundler. In the weby directory, run the following commands, one after another:

bundle add minitest

Step 2: Write the First Test

Let's write a failing test to make sure we can create and run tests.

Create a test directory in your project and add a new file called app_test.rb in it.

# weby/test/app_test.rb

require "minitest/autorun"

class AppTest < Minitest::Test
  def test_it_works
    result = 3 + 2
    assert_equal 4, result
  end
end

To run the test, we simply run the above Ruby script.

$ ruby test/app_test.rb

Run options: --seed 13065

# Running:

F

Failure:
AppTest#test_it_works [test/app_test.rb:6]:
Expected: 4
  Actual: 5


bin/rails test test/app_test.rb:4



Finished in 0.001620s, 617.2840 runs/s, 617.2840 assertions/s.
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

We can confirm that Minitest is all set up and running the tests. Very good!

However, running each test in the above way can get cumbersome. It would be nice if we could run a single command to run all tests.

Using Rake to Run Tests

Rake allows you to use ruby code to define "tasks" that you can run in the command line. Let's install Rake to create a task that will run the tests.

bundle add rake

Next, add a Rakefile in the weby directory as follows:

# Rakefile

require "minitest/test_task"

Minitest::TestTask.create(:test) do |t|
  t.libs << "test"
  t.libs << "lib"
  t.warning = false
  t.test_globs = ["test/**/*_test.rb"]
end

task :default => :test

Note: The lib directory doesn't exist yet, but let's add it anyway as we'll need it in future.

That's it. The last line marks the test task as default task, so you could run the tests using either of the following commands:

bundle exec rake

# or

bundle exec rake test

Nice! There's one improvement we could still make by making the output coloured and exciting with a progress bar.

Make Tests Pretty with Minitest Reporters

Let's add the minitest-reporters gem which prettifies the test results with coloured output and a progress bar.

bundle add minitest-reporters

Next, let's use the reporters in our test file as follows:

require "minitest/autorun"
require "minitest/reporters"

Minitest::Reporters.use!

class AppTest < Minitest::Test
  # ...
end

Now run the test again and watch the colorful output. Pretty nice!

Support Integration Testing with Rack::Test

So far, we've added a test framework and written a simple test. However, to make it work like integration tests in Rails, we need the ability to simulate an HTTP request. That is, make the request to our application, passing the required headers and parameters, receive the HTTP response, and inspect response to assert it is correct.

We'll add the support using the rack-test gem, which is a small, simple testing API for Rack apps. It can be used on its own or as a reusable starting point for Web frameworks and testing libraries to build on. In fact, rails uses rack-test behind the scenes.

Let's add rack-test to our application.

bundle add rack-test

Next, we'll require it in the test file as follows:

# test/app_test.rb

require "rack/test"
... 

The final step is to use the methods provided by this gem such as get, post, and so on to implement integration testing for our application.

If you check out the docs for the rack-test gem, you'll notice that we need to implement two things before we can use the helper methods:

  1. Include the Rack test methods.
  2. Provide the Rack application via the app method.
# Rack-Test Example

class HomepageTest < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    lambda { |env| [200, {'content-type' => 'text/plain'}, ['All responses are OK']] }
  end

  def test_response_is_ok
    # Optionally set headers used for all requests in this spec:
    #header 'accept-charset', 'utf-8'

    # First argument is treated as the path
    get '/'

    assert last_response.ok?
    assert_equal 'All responses are OK', last_response.body
  end
end

Luckily, we have our Rack app ready to go. So let's require it, include the Rack test methods, and make an HTTP request. Here's the complete test code.

# test/app_test.rb

require "rack/test"
require "minitest/autorun"
require "minitest/reporters"

require_relative "../app"

Minitest::Reporters.use!

class AppTest < Minitest::Test
  include Rack::Test::Methods

  def test_make_http_request
    get "/"
    assert last_response.ok?
    assert_equal "<h1>Hello World</h1>", last_response.body
  end

  private
    def app
      App.new
    end
end

Let's run the test with bundle exec rake, and ensure that it runs successfully. It does!

bundle exec rake
Started with run options --seed 56349

  1/1: [=====================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.00332s
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

Now just to be safe, let's verify that it's actually working by changing the output. Remove the surrounding header tags from the response body corresponding to the / route as follows:

# config/routes.rb

require_relative '../router'

Router.draw do
  get('/') { "Hello World" }

  # ...
end

Now run the test again. It fails as expected!

A failing integration test
A failing integration test

Congratulations, we have successfully implemented integration testing for our Ruby web application.

💡
If you're curious about how the get method works, check out the custom_request method in the rack-test gem. The

Add Syntactic Sugar with ActiveSupport

We are almost there. Let's finish by adding a nice syntactic sugar like Rails tests.

Notice that right now, we are writing our test methods using the standard Ruby method syntax.

def test_make_http_request
  get "/"
  assert last_response.ok?
  assert_equal "<h1>Hello World</h1>", last_response.body
end

However, I really like how the tests in Rails use the test method, providing the test description string, that reads like English. For example,

test "make HTTP request" do
  # ... 
end

This test method comes from the ActiveSupport framework, and isn't that complicated to implement. It formats the string to replace all the spaces with underscores, creates a method named test_xxx, and executes the provided block as the test body.

# File activesupport/lib/active_support/testing/declarative.rb, line 13
def test(name, &block)
  test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
  defined = method_defined? test_name
  raise "#{test_name} is already defined in #{self}" if defined
  if block_given?
    define_method(test_name, &block)
  else
    define_method(test_name) do
      flunk "No implementation provided for #{name}"
    end
  end
end

However, instead of rewriting that logic, we will simply reuse the method by requiring the ActiveSupport framework, as it provides a bunch of other niceties that we will need later.

Let's add the activesupport framework and require it in the test file.

bundle add activesupport

# test/app_test.rb

require "active_support"

Once that's set up, we need to inherit our test class from ActiveSupport::TestCase so we can access the test method. So replace Minitest::Test with ActiveSupport::TestCase and rewrite the test as follows.

# test/app_test.rb

# ...
require "active_support"

class AppTest < ActiveSupport::TestCase
  include Rack::Test::Methods

  test "make HTTP request" do
    get "/"
    assert last_response.ok?
    assert_equal "<h1>Hello World</h1>", last_response.body
  end
end

Rerun the test and ensure it passes as expected.

Extract Test Helper Class

Notice that our test file has become quite bloated, with a bunch of require statements and a private method that we will need whenever we write new tests. So instead of duplicating this logic, let's extract a test_helper file containing all the common testing code.

First create a test_helper.rb file under the test directory, containing the following extracted code from app_test.rb.

# test/test_helper.rb

require "rack/test"
require "active_support"
require "minitest/autorun"
require "minitest/reporters"

require_relative "../app"

Minitest::Reporters.use!

module ActiveSupport
  class TestCase
    include Rack::Test::Methods

    def app
      App.new
    end
  end
end

Notice that we have monkey-patched the ActiveSupport::TestCase class, just like Rails, so we could include the helper methods from rack-test and add the app method.

This simplifies our test code dramatically. Just require the test_helper and check out the new test. Pretty clean, right?

# test/app_test.rb

require "test_helper"

class AppTest < ActiveSupport::TestCase
  test "make HTTP request" do
    get "/"
    assert last_response.ok?
    assert_equal "<h1>Hello World</h1>", last_response.body
  end
end

Congratulations, we have successfully implemented Rails-like integration testing in our Ruby web application. I hope that gave you a better understanding of integration testing in Rails.

Now here's an exercise for you. We have only added a single test that ensures we get the correct output when we visit the home page. Add the remaining tests for the other routes we have added in the application.

Rails Companion: Build a Web App in Ruby Without Rails
In this course, we’ll build a web application in Ruby from scratch, without using Rails, to understand how web applications work and the core ideas behind Rails. In each lesson, we will build a specific feature from scratch using Ruby and understand the corresponding Rails concept in-depth.

That's a wrap. I hope you found this article helpful and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.