Request Spec Realistic Error Response
Happy Request Specs π
Letβs build some happy little trees. – Bob Ross
When using RSpec, you can create a request spec to “drive behavior through the full stack”.
I use request specs as my highest-level acceptance test for API endpoints. We’re building a system that allows users to place predefined elements on a canvas of sorts. Let’s build a test for placing a little tree on the canvas.
it "creates a happy little tree" do
post little_trees_path,
params: { tree: { disposition: "happy", x: 200, y: 100 } }
expect(response).to have_http_status(:created)
end
We will also verify that we can find the tree.
it "shows an existing tree" do
tree = FactoryBot.create(:tree, :little)
get little_trees_path(tree)
expect(response).to have_http_status(:ok)
end
Testing Accidents π
We donβt make mistakes, just happy little accidents. – Bob Ross
We also want to ensure the system responds as expected when users make happy accidents. Asking this endpoint for a tree that’s not little is not found.
it "does not find a large tree" do
tree = FactoryBot.create(:tree, :large)
get little_trees_path(tree)
expect(response).to have_http_status(:not_found)
end
This test does not pass! Not because we’re TDD-ing this and the implementation doesn’t exist. The endpoint does respond as expected if you hit it from curl or another way.
Our test raises an ActiveRecord::RecordNotFound
exception. We know that Rails has special handling to return a 404 status code in this case. However, the request spec still raises the exception.
Prior Art π
Anytime you learn, you gain. – Bob Ross
I didn’t figure out how to resolve this on my own. I only came across a solution by searching for others’ approaches. I found this issue in rspec-rails
’s issue tracker. In there Michael Gee shares a workaround. The earliest reference to this approach that I can find is from Eliot Sykes in 2017. If you’re aware of an earlier reference to this, let me know!
Painting A Realistic Picture π
All you need to paint is a few tools, a little instruction, and a vision in your mind. – Bob Ross
To get the production behavior in our test, we need to turn off Rails’ fancy exception page. Michael and Eliot share this method to do so:
module ErrorResponses
def respond_without_detailed_exceptions
env_config = Rails.application.env_config
original_show_exceptions = env_config["action_dispatch.show_exceptions"]
original_show_detailed_exceptions = env_config["action_dispatch.show_detailed_exceptions"]
env_config["action_dispatch.show_exceptions"] = :all
env_config["action_dispatch.show_detailed_exceptions"] = false
yield
ensure
env_config["action_dispatch.show_exceptions"] = original_show_exceptions
env_config["action_dispatch.show_detailed_exceptions"] = original_show_detailed_exceptions
end
end
We pass a block to this method in our test. We store the initial values of the environment configuration, then set show_exceptions
and show_details_exceptions
to match production. After our block executes, we return them to their original value.
Note that prior to Rails 7.1, you’ll want to set the value of show_exceptions
to true
. From 7.1 on, the configuration accepts a set of symbols and using true or false is deprecated.
Let’s update our test to use this method.
it "does not find a large tree" do
respond_without_detailed_exceptions do
tree = FactoryBot.create(:tree, :large)
get little_trees_path(tree)
expect(response).to have_http_status(:not_found)
end
end
Now our test passes! We’re returning a 404 and not raising an exception.
Using Your Tools π
You can do anything here β the only prerequisite is that it makes you happy. – Bob Ross
If we’re using RSpec, we have another option, which again comes from Eliot. We can use RSpec’s around
hook to pass the entire test as the block to respond_without_detailed_exceptions
by applying metadata.
module ErrorResponses
RSpec.configure do |config|
config.include self, type: :request
config.around(realistic_error_responses: true) do |example|
respond_without_detailed_exceptions(&example)
end
end
end
That allows us to rewrite our test as follows:
it "does not find a large tree", :realistic_error_responses do
tree = FactoryBot.create(:tree, :large)
get little_trees_path(tree)
expect(response).to have_http_status(:not_found)
end
Artistic Inspiration π
Donβt forget to tell these special people in your life just how special they are to you. – Bob Ross
I cannot stress enough that this post wouldn’t exist without the contributions of Michael Gee and Eliot Sykes. I’m thankful to them for sharing this solution. I’ve used this approach in various codebases in the last few years. I’ve shared this approach with many people (and now with you). This post serves to draw attention to their work.