Request Spec Realistic Error Response
Tags: ruby, testing, rails
Published: 2022-07-17 11:51:44

## Happy Request Specs

> Let’s build some happy little trees.
> -- [Bob Ross](https://parade.com/1215163/kelseypelzer/bob-ross-quotes/)

When using RSpec, you can create a [request spec](https://relishapp.com/rspec/rspec-rails/docs/request-specs/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.

```ruby
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.

```ruby
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](https://parade.com/1215163/kelseypelzer/bob-ross-quotes/)

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.

```ruby
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](https://curl.se/) or another way.

Our test raises an `ActiveRecord::RecordNotFound` exception. We know that Rails has [special handling](https://dev.to/thegnarco/w-rapping-about-exceptional-behavior-in-rails-2c2k#research) 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](https://parade.com/1215163/kelseypelzer/bob-ross-quotes/)

I didn't figure out how to resolve this on my own. I only came across a solution by [searching](https://dev.to/kevin_j_m/searching-for-a-reason-32d3) for others' approaches. I found [this issue](https://github.com/rspec/rspec-rails/issues/2024) in `rspec-rails`'s issue tracker. In there [Michael Gee](https://github.com/mikegee) shares a workaround. The earliest reference to this approach that __I__ can find is from [Eliot Sykes](https://eliotsykes.com/2017/03/08/realistic-error-responses/) 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](https://parade.com/1215163/kelseypelzer/bob-ross-quotes/)

To get the production behavior in our test, we need to turn off Rails' [fancy exception page](https://www.honeybadger.io/blog/how-rails-fancy-exception-page-works/). Michael and Eliot share this method to do so:

```ruby
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"] = true
    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](https://dev.to/thegnarco/temporary-state-in-tests-3l6l) 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.

Let's update our test to use this method.

```ruby
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](https://www.pinotspalette.com/logansquare/blog/pinot-state-of-mind/bob-ross-quotes)

If we're using RSpec, we have another option, which again comes from [Eliot](https://eliotsykes.com/2017/03/08/realistic-error-responses/). We can use RSpec's `around` [hook](https://relishapp.com/rspec/rspec-core/v/2-0/docs/hooks/around-hooks) to pass the entire test as the block to `respond_without_detailed_exceptions` by applying metadata.

```ruby
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:

```ruby
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](https://parade.com/1215163/kelseypelzer/bob-ross-quotes/)

I cannot stress enough that this post wouldn't exist without the contributions of [Michael Gee](https://github.com/mikegee) and [Eliot Sykes](https://eliotsykes.com). 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.