How to Properly Test a Rails API with Rspec

Sandy Edwards
4 min readDec 18, 2018

Hi, programming world! Today I’m going to cover what I consider to be the best way to handle writing tests for the GET, POST, PUT and DELETE routes on your RESTful Ruby on Rails API.

A Few Good Gems

In order to run our tests, populate them with dummy data and then clean the temporary database, we will need to add a few new gems to the development, test namespace in our gemfile.

group :development, :test do
gem 'rspec-rails', '~> 3.6'
gem 'faker'
gem 'factory_bot_rails'
gem 'database_cleaner'
end

Add the gems listed above and run bundle install in your terminal while in the API’s root folder.

File & Folder Setup

Ruby on Rails creates a tests folder by default. Since we will be using the Rspec gem for our Rails API’s tests, we’ll want to get rid of that folder and proceed to initialize an rspec directory in our Rails via the terminal. Run the following code in the command line:

rails generate rspec:install

This will create a spec folder in the root of your project that initializes with a rails_helper.rb and spec_helper.rb file.

By default, Rspec resets or ‘cleans’ the database after each test method is ran. However, in some of our methods we will be seeding data in a way that Rspec does not know how to properly clean up. Because of this, we will be using the database-cleaner gem to insure that none of our test data overlaps and causes false negatives in our tests.

Inside the spec/rails_helper.rb file, set the line of code that reads:

config.use_transactional_fixtures = true

to:

config.use_transactional_fixtures = false

Below that line, we’ll add the configs for the database-cleaner.

config.use_transactional_fixtures = falseconfig.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
config.before(:all) do
DatabaseCleaner.start
end
config.after(:all) do
DatabaseCleaner.clean
end

Factory Setup

Once those steps are complete, we can begin setting up a factory file which will contain some methods for dynamically creating dummy seed data for our tests. Create a file in your spec folder called factories.rb.

Inside the factories.rb file, we’ll add some code to create a dummy random Question instance in our database.

FactoryBot.define do
service_array = ["Test Service", "Test Service Two"]
letter = ["a", "b", "c", "d"]
factory :random_question, class: Question do
question { Faker::Lorem.question }
answer { Faker::Lorem.sentence }
service { service_array.sample }
number { Faker::Number.between(1, 2) }
letter { letter.sample }
end
end

Here we are using the Faker gem library for generating dummy data. For a full list of Faker data options, see the docs: https://github.com/stympy/faker

We’ve created a method that generates a Question instance with a random lorem ipsum question, answer, random number, and samples from our service and letter arrays.

And Now for the Tests

Create a folder called requests in the spec folder, this is where we will create our test files.

Make a test file for the the get all tests route called get_questions_spec.rb. Be sure to include the ‘_spec.rb’ at the end, as this allows Rspec to detect it as a test file.

Inside the file, add this code:

require 'rails_helper'describe "get all questions route", :type => :request do
let!(:questions) {FactoryBot.create_list(:random_question, 20)}
before {get '/api/v1/questions'}it 'returns all questions' 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

First, we create 20 random questions in the database using the factory bot. Then we right a few assertions testing the size of the get API call’s response, and status code.

Next, create a post_question_spec.rb file for the new question route.

require 'rails_helper'describe "post a question route", :type => :request dobefore do
post '/api/v1/questions', params: { :question => 'test_question', :answer => 'test_answer', :service => 'test_service', :number => 2, :letter => 'a' }
end
it 'returns the question' do
expect(JSON.parse(response.body)['question']).to eq('test_question')
end
it 'returns the question\'s answer' do
expect(JSON.parse(response.body)['answer']).to eq('test_answer')
end
it 'returns the question\'s service' do
expect(JSON.parse(response.body)['service']).to eq('test_service')
end
it 'returns the question\'s letter' do
expect(JSON.parse(response.body)['letter']).to eq('a')
end
it 'returns the question\'s number' do
expect(JSON.parse(response.body)['number']).to eq(2)
end
it 'returns a created status' do
expect(response).to have_http_status(:created)
end
end

Here we are making an API call to the new Question route with fake seed data and testing if the response matches the provided data.

Next, for the update Question route create put_question_spec.rb.

require 'rails_helper'describe "PUT /api/v1/questions/:id" dobefore(:each) do
@question = create(:random_question)
end
it 'updates a question' do@new_question = Faker::Lorem.question
@new_answer = Faker::Lorem.sentence
put "/@question.id">api/v1/questions/#{@question.id}", params: {question: @new_question, answer: @new_answer}expect(response.status).to eq(202)
expect(Question.find(@question.id).question).to eq(@new_question)
expect(Question.find(@question.id).answer).to eq(@new_answer)
end
end

Here we send a PUT request to the API with some generated Faker data and assert that the response matches the original data.

Finally, the DELETE question route. Inside delete_option_spec.rb add this code:

require 'rails_helper'describe "delete question route" do
before(:each) do
@question_one = create(:random_question)
@question_two = create(:random_question)
end
it 'should delete the question' doget "/api/v1/questions"
expect(response.status).to eq(200)
expect(JSON.parse(response.body)).to eq([YAML.load(@question_one.to_json),YAML.load(@question_two.to_json),])
delete "/api/v1/questions/#{@question_one.id}"
expect(response.status).to eq(204)
get "/api/v1/questions"
expect(JSON.parse(response.body)).to eq([YAML.load(@question_two.to_json)])
end
end

Just as we did with the other routes, we create an API request with some Faker data and compare the response with the original data.

And that should do it for testing your Rails API!

Thanks for reading…

Love Code // Love Life

--

--

Sandy Edwards

Software Engineer at Subtext, Flatiron School Grad, French Bulldog Father https://sandyedwards.io