Jack Flannery.com

Test Driving a Rails API with minitest

by

Why test?

Making a habit of using tests to drive development is not only great for productivity, but it can also take developer happiness to the next level. There are so many benefits to it, not only do you get a regression test suite, TDD also saves you from having to constantly reload and visually test your program. Your tests serve the purpose of validating your code at every step of the way. You can create entire API's without firing up your browser, or API testing tool, though you should use those things as a final test at the end. You don't need those tools during development, all you need are your tests. Your tests will become the first client of your code, they will inform you how difficult or easy for code is to use.

This article uses ruby 2.6.1 and rails 5.2.2:

$ ruby -v
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin18]
$ rails -v
Rails 5.2.2

In this article we'll be using Rails to create a web API for a blogging application. An API, or application programming interface, is what allows programs to interact with each other. This web API will serve as the backend of a blogging application. A frontend application would interface with the backend via our web API. In future articles, I'll demonstrate how a front end application interacts with the backend using a web API, but here we'll just be building the backend.

We'll use the REST architectural style to build our web API, which deals in resources. A RESTful resource can be any object in the domain of your program. The main resource we'll be using in this article will be blog posts. RESTful resources expose a set of URL's that can be used to perform various operations on a resource. Typically those operations are create, read, update, and delete; often referred to as CRUD. So we will build a web API which provides URL's to perform the basic CRUD operations on blog posts. The nice thing about REST is that it leans heavily on the web semantics that we already know and love. To retrieve a post or posts, we will expose a URL endpoint that uses the GET http verb. The URL for creating a post will use the POST verb. The update URL will use the PUT or PATCH verbs, and for deleing a post, we'll use the DELETE verb. As this is an API, these URL's are not meant for human consumption, they are meant to be used by other programs.

Building a web API with Rails only require a subset of Rail's features. Specifically, we don't need the view layer, since we will not be rendering HTML pages, as a traditional Rails app does. Our web API endpoints will instead only exchange JSON data. We also won't need the asset pipeline, since there is no Javascript or CSS needed for rendering JSON data. Luckily, recent versions of Rails gives you the option to make your application API only, so the parts of Rails we don't need won't be included. You may be wondering if it's worth putting the time and effort into testing this, since Rails makes it so easy to create a simple CRUD web API. I think it is worth it. I mainly use high level integration tests for testing API's. Only when custom logic is needed in a model, would I drop down to unit testing. There are many things we can test and verify with integration tests: request parameters, HTTP status codes and headers, response payloads and authentication. We won't be covering authentication in this article, but I will be covering that in a future article.

Remember, the tests are first and foremost there to drive development. Later we will use them as a regression test suite. Lets get started by creating a new rails application:

$ rails new poster --api -d postgresql --skip-spring

Let's break down the options passed to rails new poster. The --api options tells rails that we do not want a full rails app, but we want our app to be API only. -d postgresql says that we want to use postgresql as our database. --skip-spring will cause rails to generate our app without including spring. Spring is whats known as an application preloader. Without spring, every time you run a test or a rake task, you have to wait for your rails app to start up. Spring aims to save you time by keeping your app running in the background so tests and rake task start running much faster. And that's true, spring will cut out the start up time when running tests, and I like to run tests often during development, so that time savings can really add up. However, it could also cause problems. I've had tests fail and it turned out to be spring not loading new code. If you're following along, feel free to try it out and decide for yourself if it's worth using.

rspec is neat, but in this article I'll keep it simple and use minitest. When it comes to testing a rails API, the most important gem we'll need is rack-test. Standard rails applications that render an html view can be integration tested with capybara. Rails now provides this built in with system tests. Capybara provides methods to programmatically browse to your application, interact with it's inputs, and test the output from a user perspective. An api doesn't have any views to interact with so we won't be needing capybara. rack-test provides the methods we'll need to make requests to our API, set request headers, send parameters and make assertions about the results.

Add rack-test to your Gemfile under the development/test group and require it like so:

group :development, :test do
  gem 'rack-test'
end

Run bundle install or simply bundle in your terminal to install it.

Lets also create the test and development databases:

$ rails db:create

I like to use minitest spec syntax in my tests so, in test/test_helper.rb I'll add require minitest/spec' and extend Minitest::Spec::DSL. I do not use fixtures, instead prefer factories, so I'll remove the fixture setup code. Your test helper should now look like this:

# test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
require 'minitest/spec'
require 'minitest/autorun'

class ActiveSupport::TestCase
  extend Minitest::Spec::DSL
   
  # Add more helper methods to be used by all tests here...    
end

Rails will generate fixtures each time we generate a new model. Let's save ourselves the hassle of having to remove those fixture files each time by telling rails that we don't want them. Add this configuration to config/application.rb inside the Application class:

config.generators do |g|
  g.test_framework  :test_unit, fixture: false
end

That's about all the extra setup we need. We're now ready to implement our first API endpoint. We'll start with the most basic endpoint, GET /posts. We'll start by writing a GET posts integration test. Notice that we haven't created any models or controllers yet. But we have a good idea of what we want our API to return, so we'll describe that in a test. Lets use the tools rails gives us and generate a test skeleton:

$ rails g integration_test posts

The newly generated test/integration/posts_test.rb looks like this:

# test/integration/post_test.rb
require 'test_helper'

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

Notice that the test class inherits from ActionDispatch::IntegrationTest. This is the rails standard integration test class. It allows us to make requests to our application from the outside, exercising the entire stack.

Let's start with a very simple first test that will lead us to adding that first endpoint. Add the following code to test/models/post_test.rb:

# test/integration/post_test.rb
require 'test_helper'

class PostsTest < ActionDispatch::IntegrationTest

  describe 'GET posts' do

    it 'responds with 200 OK' do
      get '/posts'
      response.status.must_equal 200
    end
  end
end

The get '/posts' part of the test is made possible by the rack-test gem that we added earlier. That line will make a request to the application just as if it came from an actual browser.

Run the whole test suite with:

$ rake test

You can also run a single test file:

$ ruby -Itest test/integration/posts_test.rb

Running the test yields an error: ActionController::RoutingError: No route matches [GET] "/posts". Listen to the tests, they tell us what to do next. They're telling us to create the route. We can generate a controller and route with this command:

$ rails g controller posts index

That command generated some code for us, most importantly, we now have a PostsController with an index method. It also added the route into the routes file, config/routes.rb. Take a look at the routes file and you'll see that get 'posts/index' was added. We don't want the url to be /posts/index, what we want is /posts, so replace that line with:

get 'posts', to: 'posts#index'

What that line does is map the /posts url, to the PostsController::index method, also know as the index action. Run the tests again we should see a test failure:

Expected: 200
Actual: 204

Status code 204 means No Content. Thats the status code you'd expect when the response payload is blank, which it currently is. We're expecting a JSON payload with a 200 OK status code. Let's fix that, open app/controllers/posts_controller.rb and edit the index method so it looks like this:

def index
  render json: []
end

This is just a dummy JSON response for now. Since the controller is rendering some JSON, rails will set the response status code to 200. Run the tests, and they should pass.

Rendering an empty JSON array is not very interesting. Let's add another test describing the real response that we want the endpoint to return. The test will need some test data, so we'll need to create a couple post records in a before block. Then we'll add a second test, that asserts that response payload contains the test post data. The integration test should now look like this:

class PostsTest < ActionDispatch::IntegrationTest

  describe 'GET posts' do
    before do
      Post.create!(title: 'Hello World', content: 'This is my first post.')
      Post.create!(title: 'How to TDD', content: 'Failing Test. Make it pass. Refactor. Repeat')
    end

    it 'responds with 200 OK' do
      get '/posts'
      response.status.must_equal 200
    end

    it 'returns all posts' do
      get '/posts'
      json = JSON.parse(response.body)
      json.size.must_equal 2
      json[0]['title'].must_equal 'Hello World'
      json[0]['content'].must_equal 'This is my first post.'
      json[1]['title'].must_equal 'How to TDD'
      json[1]['content'].must_equal 'Failing Test. Make it pass. Refactor. Repeat'
    end
  end
end

I mentioned earlier that I use factories so why not here? I do use factories quite often, but I don't reach for them in every situation. Tests should give you a feel of how it is to work with your code, and creating new Post objects is part of the API of your code. Let's put it all out there for future readers of these tests, and not hide the details of creating Posts somewhere else.

The test simply makes a call to /posts and expects to get back two posts in the response body, and that they have the same title and content as the ones we created in the before block. Because we are using rack-test we use response.body to check the payload.

Executing the test yields an error: NameError: uninitialized constant PostsTest::Post. This is because we haven't created the Post model or the posts database table yet. Lets do that now using a generator:

$ rails g model Post title:text content:text

This will generate the Post model class definition

class Post < ApplicationRecord
end

The Post class inherits from ApplicationRecord, which itself inherits from ActiveRecord::Base, which makes it an ActiveRecord model. By naming the the class Post, ActiveRecord automatically connects it with the posts database table, no configuration is needed. Even though our class definition is empty, ActiveRecord gives the Post class lots of useful methods for interacting with the posts table. That includes the Post.create! method that we just used in the test to create test data.

The model generator also created a database migration file. The database migration contains the commands needed to generate the posts table. We told the model generator that we wanted the posts table to contain two columns of type text: title and content. This is the recommended data type for most textual data when using Postgresql. Run the migration to create the posts table.

$ rails db:migrate

If you run the tests at this point, you'll see a test failure on line 19:

Expected: 2
Actual: 0

We are expecting two posts in the response but got none. We'll now replace the dummy response with the real one. The real response should be a JSON array of all post records that exist in the posts table, We can select all posts with ActiveRecord's all method which exists on the Post class. The PostsController::index method should now look like this:

def index
  render json: Post.all
end

And with that, we should see two green dots and no red; we officially have two passing tests in the test suite.