Blog

Easy Request Specs with “let”

Request specs allow for higher-level testing of a Rails application (as compared to controller specs, for example). They still run in the same process as your application code, but instead of directly instantiating classes and invoking methods, they make requests to endpoints and validate the responses.

Request specs can be fairly complex, making several requests with assertions all along the way. They can also be simple, making a single request and verifying the response. This is a pattern for structuring simple request specs that makes them really easy to write.

As an exercise, I’ll write a quick spec for a fictional book-search endpoint.

The endpoint URL looks like this:

http://application/search?title={string}

And the basic structure of the spec looks like this:


describe 'Book Search', :request do
  before do
    # create some model objects to look for when searching:
    Book.create(title: 'A Wrinkle in Time')
    Book.create(title: 'A Brief History of Time')
    Book.create(title: 'A Short History of Nearly Everything')
  end

  # Examples and stuff go here...
end

 

With the foundation in place, let’s start to building the out spec. Since this is a request spec, every example is going to send a request to the search endpoint. To keep things simple, I’d like to use a single top-level before block to make the request:


describe 'Book Search', :request do
  ...
  let(:base_url) { '/search' }
  let(:params) { {} }
  
  before do
    get "#{base_url}?#{params.to_query}"
  end

  context 'without any params' do
    it 'should return an "invalid request" error'
  end
end

 

That takes care of the case where there are no parameters, but I think I’d like a little more coverage than that. For my other examples, I’ll need to vary the request parameters, but I’d still like to make the requests in that same before block. Is there a way do that? Yes! It turns out that since params is defined using let, I can redefine it in sub-describe and sub-context blocks to fit my needs.

In addition to redefining params inside the inner describe block, I’ll also define all of the variables that I want to play with in that describe block using lets. Then I’ll redfine those variables with specific values in sub-contexts.

For example, let’s look at searching using the title parameter. I’ll start with a describe block for it:


describe 'Book Search', :request do
  ...
  describe 'by title' do
    let(:title) {}
    let(:params) { { title: title } }
    
    # Any examples in this block will have this new `params` definition...
  end
end

 

Here, I’ve defined a place-holder title, and redefined params so that it includes that title. For the examples inside this block, that original before block will use this newly-redefined params and send along whatever value I specify for title. The interesting part of this is that params will use the redefined values for title, without be redefined itself.

But, I still haven’t actually specified any values for title yet. For each different value of title that I want to use, I’ll define a sub-context:


describe 'Book Search', :request do
  ...
  describe 'by title' do
    ...
    context 'with an empty value' do
      let(:title) { '' }

      it 'should return an "invalid request" error'
    end

    context 'with a stop word' do
      let(:title) { 'of' }
      
      it 'should ignore the stop word and return an "invalid request" error'
    end

    context 'with a non-matching value' do
      let(:title) { 'arugula' }
      
      it 'should not return any results'
    end

    context 'with a stop word and non-matching value' do
      let(:title) { 'of arugula' }
      
      it 'should ignore the stop word and not return any results'
    end
    
    context 'with a matching value' do
      let(:title) { 'history' }

      it 'should return only the books with titles containing the value'
    end

    context 'with transposed letters' do
      let(:title) { 'hitsory' } # The 't' and 's' are transposed.
      
      it 'should match as though the letters were not transposed'
    end
  end
end

 

Notice that I didn’t need to redefine params in each sub-context. In the describe block I defined params to contain the value of title, so in each sub-context, I only have to define title.

When the top-level before hook runs, it generates URLs like /search?title=history and /search?title=arugula by resolving the layered let definitions.

This pattern can be used for more than just request specs, too. You can use lets for method arguments and then use sub-contexts for each specific argument value (or combination of argument values).

Patterns like this are my favorite way of keeping code clean. There’s a careful balance between “not repeating yourself” and “creating obfuscated code that no one can understand.” This pattern gets rid of boilerplate code, which makes the resulting code not only smaller, but easier to understand. Because of that, it’s also easier to extend the spec with new examples. Instead of copying, pasting and hand-editing a bunch of supporting code, you simply create a new context, declare the parameters, and write expectations.