Lesson Weekend

Just as with any other coding project, we should always use Test Driven Development when building a Rails API. Write the test first, make sure it fails, then add just enough code to make that test pass.

Perhaps you're getting tired of writing specs. Maybe you've fallen into the bad habit of writing your code first and then writing specs later, making them an afterthought. Or maybe you're not writing many specs at all. That's a bad idea. Many tech companies won't let junior developers touch the codebase until they gain more experience — often by testing the code. In other words, being a good tester is invaluable to getting a tech job.

Make sure you continue to write good tests. They'll help you break large, overwhelming problems into smaller, more manageable puzzles.

Request Specs


We will use integration specs to test our APIs. Instead of using Capybara (which isn't designed for API testing), we'll use RSpec to make request specs. A request spec pushes behavior through the stack so it hits our API endpoints, a necessary part of properly testing an API. Check out RSpec's documentation on request specs for more information.

Let’s go through a few examples of request specs to get you started. We'll start by creating a new folder called spec/requests. Next, we’ll create a file for testing the GET route that returns all quotations.

spec/requests/get_quotes_spec.rb
require 'rails_helper'

describe "get all quotes route", :type => :request do
  let!(:quotes) { FactoryBot.create_list(:quote, 20)}

  before { get '/quotes'}

  it 'returns all quotes' do
    expect(JSON.parse(response.body).size).to eq(20)
  end

  it 'returns status code 200' do
    expect(response).to have_http_status(:success)
  end
end

Let's go over new terminology here. First, we use :type => :request to specify that this is a request spec that drives behavior through the full stack. By "full stack," we mean that we aren't just testing the controllers. We're testing the routing as well. We'll also get the added benefit of ensuring that everything in our application is working correctly, including any methods we're calling that could be related to services, models, and so on.

We're also using let!, an RSpec helper that allows us to save the value of :quotes across each test in our describe block. We discussed this line of code in the last lesson — and we are pulling from our factory to generate these quotes. (Don't forget that you need a factory for this to work — review the last lesson if you haven't added a factory for quotes yet.)

Next, we have a before do block that will return the GET '/quotes' route before each test. (A quick reminder: the {} is shorthand for a do/end block that is written on one line.)

Our first test parses the response and expects the size of the response body to be twenty results. This confirms that all twenty quotes FactoryBot has created are being returned through our GET '/quotes' call.

The second test confirms that our API returns the correct header. We use a new RSpec method here: have_http_status. Check out the documentation on this method and others like it.

We should always test to ensure that a route returns both the correct response body and the correct header. For now, this is all we need to test our GET '/quotes' route. We don't have any exceptions to test, but if we did, we'd need to test the response body and the header for those, too.

For instance, if we added authentication to our API and only authenticated users could access this route, we'd also need to test the response message and header 401 - Not Authorized that would be returned for unauthenticated users.

Let's check out another example. Here are a few tests for our POST route:

spec/requests/post_route_spec.rb
require 'rails_helper'

describe "post a quote route", :type => :request do

  before do
    post '/quotes', params: { :author => 'test_author', :content => 'test_content' }
  end

  it 'returns the author name' do
    expect(JSON.parse(response.body)['author']).to eq('test_author')
  end

  it 'returns the quote content' do
    expect(JSON.parse(response.body)['content']).to eq('test_content')
  end

  it 'returns a created status' do
    expect(response).to have_http_status(:created)
  end
end

Once again we specify this is a request spec. Before each test, we'll POST an author and content with the params we've included. Then we'll test that each response field has the correct value and we also confirm that the response returns the correct header.

So now we're all done testing the POST route, right? No, not quite. We have exceptions to test as well. What happens if a user makes a POST call that doesn't include the author? We'll need to test the response and header for this exception as well.

This should be enough to get you started with writing your own thorough tests. Once again, make sure to test all routes and all exceptions. There are no exceptions to this rule :)

Further Exploration


Can't get enough testing? Here are some suggestions for further exploration.

  • Learn how to write better RSpec tests. There are some great tips here. At the very least, you should know how to use context in an RSpec test.

  • As you write more tests, you'll soon realize that the following method is being used over and over: JSON.parse(response.body). That's not very DRY. Well, specs can utilize helper methods and modules, too. Refactor your tests by creating a module with a json helper method. You'll need to add some configuration to spechelper.rb_ to make this work.

  • Take the time to do your own research on testing Rails applications, including reading blogs on the topics. You'll learn other testing best practices as well. That way, even if a potential employer won’t let you touch the codebase, you can still get your foot in the door by testing the codebase.

Sample request specs

spec/requests/get_quotes_spec.rb
require 'rails_helper'

describe "get all quotes route", :type => :request do
  let!(:quotes) { FactoryBot.create_list(:quote, 20)}

  before { get '/quotes'}

  it 'returns all quotes' do
    expect(JSON.parse(response.body).size).to eq(20)
  end

  it 'returns status code 200' do
    expect(response).to have_http_status(:success)
  end
end
spec/requests/get_quotes_spec.rb
require 'rails_helper'

describe "get all quotes route", :type => :request do
  let!(:quotes) { FactoryBot.create_list(:quote, 20)}

  before { get '/quotes'}

  it 'returns all quotes' do
    expect(JSON.parse(response.body).size).to eq(20)
  end

  it 'returns status code 200' do
    expect(response).to have_http_status(:success)
  end
end

Lesson 8 of 19
Last updated August 7, 2022