Request Specs
Last Updated:
Request specs are integration tests for a Rails API. They act as a mini “feature” test for an endpoint or group of endpoints. Request specs test the whole stack including routing, middleware, and rack request and responses. Whereas a controller spec is a unit test for a controller action(s). RSpec recommends request specs over controller specs because they are faster and are more realistic. APIs usually only have request specs.
Unconventional example
Request specs aren’t a better unit test for controllers. In practice request specs typically align with a controller because they both handle a group of related endpoints. It’s best that this correlation is implied rather than explicit. Controller specs test actions and request specs test endpoints. As controllers and endpoints drifts from the conventions, the difference between what request specs and controllers test becomes more apparent.
Here are an example to demonstrate the difference in what’s being tested
# app/controllers/v1/cars_controller.rb
class V1::CarsController < ApplicationController
# GET v1/automobiles
def index
render json: Vehicle.all.to_json
end
# POST v1/automobiles
def create
vehicle = Vehicle.create(car_params)
render json: vehicle.to_json
end
end
In the API controller you can see three potential options for the request spec naming. First the controller name: V1::CarsController
. Second the object (or concept) involved: Vehicle
. Third is the endpoint: v1/automobiles
.
Ideally each component of the stack would be named using “vehicle”, so naming the request spec is obvious. In practice that doesn’t always happen, so it’s better to use the name from what’s being tested to avoid confusion and prevent guessing. In the above case we are testing the automobiles endpoints.
The corresponding request spec should look like this
# spec/requests/v1/autombiles
Rspec.describe 'V1::Automobiles', type: :request do
describe 'GET v1/automobiles' do
end
describe 'POST v1/automobiles' do
end
end
Best practices
Naming
Request spec's path should be based on route namespaces/scopes/nested resources
Request spec's filename should be based on the object, conceptual idea, or last path in route
Request spec's top-level example group name should be based on file path & name
Route | /v0/messaging/health/messages/:id/attachments/:id |
Namespaces | /v0/messaging/health/messages |
Objects/last path | attachments |
translates to:
File path | spec/requests/v0/messaging/health/messages |
File name | attachments_spec.rb |
Example group name | 'V0::Messaging::Health::Messages::Attachments' |
Exceptions
If the entire stack uses the same root or base name (e.g., Vehicle) except for the routes (e.g., v1/automobiles) it’s acceptable to use Vehicle instead of Automobile for naming the request spec. In this case, it’s better to have one exception instead of two.
Nested example group names
Nested example groups should test one specific endpoint. The name should include only the HTTP verb and the path. Context groups can be used if further nesting is needed to test different states or situations within the nested describe group.
# bad
describe '#index' do
describe 'get all users'
describe 'when params are valid return all users'
# good
describe 'POST api/v1/users' do
context 'when params are valid' do
end
context 'when params are invalid' do
end
end
Multi-controller request specs
Although request specs typically only involve one controller, it can test multiple controllers that have related endpoints. In this case, naming can be more generic or focus on the common namespace. Here’s an example of how this would be done.
You have a chat app with messages. You may have the following controllers:
MessagesController
Messages::DraftsControllers
Messages::TrashesController
Messages::UnreadsController
Draft, trash, unread are states or scopes of a message, not other other models. So, it would be reasonable to create a single request spec that tests these related endpoints:
# spec/requests/messages_spec.rb
Rspec.describe 'Messages API', type: :request do
describe 'GET messages' do
end
describe 'POST messages' do
end
describe 'PUT messages' do
end
describe 'DELETE messages' do
end
describe 'GET messages/drafts' do
end
describe 'GET messages/trashes' do
end
describe 'GET messages/unreads' do
end
end
Although they don’t need to be under the same namespace, the common namespace is an indicator that the endpoints could be tested in a single spec. Related endpoints can use the same spec file as long as they are conceptually closely related. When in doubt use separate files.
Defining related endpoints
This means the endpoints have the same or nearly the same conceptual object.
Let’s consider a couple examples.
Associations
v1/engines
v1/engines/:id/parts
In this example an engine has many parts and the parts are nested resources in an engine. They might seem closes related because of the nesting, but are different conceptual objects. A fuel injector isn’t an engine and an engine isn’t just a fuel injector. They should have separate files.
Beyond CRUD
v1/users
v1/users/search
v1/users/filter
In this example you could have three controllers for users, searching, and filtering. Conceptually, they involve the same conceptual object, User. These three sets of endpoints could use the same spec file. Another indicator for a single file is a lack of associations. If has_many
or belongs_to
are involved in the conceptual object, then use different files. Note: just because they could use the same spec files, doesn’t mean they need to use the same file.
Single Controller and Multiple Objects
Sometimes a single controller returns multiple objects or models. At first glance, these endpoints seems closely related because they are in the same controller. However, FormA and Attachment are separate conceptual ideas. They should have separate request specs (and should be in different controllers.)
class V1::FormA < ApplicationController
# GET v1/form_a/:id
def show; end
# POST v1/form_a
def create; end
# GET v1/form_a/:id/attachment
def attachments; end
end