How to Properly Test a Rails API with Rspec
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)
endconfig.before(:each) do
DatabaseCleaner.strategy = :transaction
endconfig.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
endconfig.before(:each) do
DatabaseCleaner.start
endconfig.after(:each) do
DatabaseCleaner.clean
endconfig.before(:all) do
DatabaseCleaner.start
endconfig.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)
endit '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' }
endit 'returns the question' do
expect(JSON.parse(response.body)['question']).to eq('test_question')
endit 'returns the question\'s answer' do
expect(JSON.parse(response.body)['answer']).to eq('test_answer')
endit 'returns the question\'s service' do
expect(JSON.parse(response.body)['service']).to eq('test_service')
endit 'returns the question\'s letter' do
expect(JSON.parse(response.body)['letter']).to eq('a')
endit 'returns the question\'s number' do
expect(JSON.parse(response.body)['number']).to eq(2)
endit '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)
endit 'updates a question' do@new_question = Faker::Lorem.question
@new_answer = Faker::Lorem.sentenceput "/@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)
endit '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)])
endend
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…