DRY Request Specs with different user contexts

45 Views Asked by At

I am working on an api app that for this example has 2 basic user types, admin and user.

For this example the resource will be Widget. An admin has full access to a widget in the controller and a user has no access. Right now, I am repeating myself quite a bit when writing the request specs.

EG

require 'rails_helper'

RSpec.describe '/widgets', type: :request do
  context 'with an admin user' do
    let(:user) { create(:user, is_admin: true) }
    let(:valid_attributes) { attributes_for(:widget) }
    let(:invalid_attributes) { attributes_for(:widget, name: nil) }
    let(:valid_headers) { Devise::JWT::TestHelpers.auth_headers({}, user) }

    describe 'GET /widgets' do
      it 'renders a successful response' do
        create(:widget)
        get widgets_url, headers: valid_headers, as: :json
        expect(response).to be_successful
      end
    end
  end

  context 'with a standard user' do
    let(:user) { create(:user) }
    let(:valid_attributes) { attributes_for(:widget) }
    let(:invalid_attributes) { attributes_for(:widget, name: nil) }
    let(:valid_headers) { Devise::JWT::TestHelpers.auth_headers({}, user) }

    describe 'GET /widgets' do
      it 'renders an unauthorized response' do
        create(:widget)
        get widgets_url, headers: valid_headers, as: :json
        expect(response).to have_http_status(403)
      end
    end
  end
end

Now imagine this all done over the entire CRUD of the API and my request specs are 100s of lines long with a lot of copy pasting, they are hard to read and will be kind of difficult to manage.

Most of the boilerplate is the same, I am just wanting to implement different user contexts to verify that my policies are taking effect.

In a perfect world I would be able to define describe and context blocks once and then just make different expectations based on the user context.

Any suggestions on where to look?

2

There are 2 best solutions below

1
Marian Theisen On

you can pull up most of your lets, as they are lazily evaluated anyway. e.g.:

require 'rails_helper'

RSpec.describe '/widgets', type: :request do
  let(:valid_attributes) { attributes_for(:widget) }
  let(:invalid_attributes) { attributes_for(:widget, name: nil) }
  let(:valid_headers) { Devise::JWT::TestHelpers.auth_headers({}, user) }

  context 'with an admin user' do
    let(:user) { create(:user, is_admin: true) }

    describe 'GET /widgets' do
      it 'renders a successful response' do
        create(:widget)
        get widgets_url, headers: valid_headers, as: :json
        expect(response).to be_successful
      end
    end
  end

  context 'with a standard user' do
    let(:user) { create(:user) }


    describe 'GET /widgets' do
      it 'renders an unauthorized response' do
        create(:widget)
        get widgets_url, headers: valid_headers, as: :json
        expect(response).to have_http_status(403)
      end
    end
  end
end

I would advisee against "DRYing" up further. Especially tests should be verbose about the functionality that they are ensuring.

2
smathy On

You want to be using shared examples, and maybe even push this off into a separate file that you include with require_relative and include_context

It's gonna be something like this (note that I also override the subject to make the request and return the request so you can use the is_expected. shorthand syntax - that's not required if you prefer using the longer expect(response). syntax)

describe '/widgets', type: :request do
  let(:user_params) {{}}
  let(:user) { create(:user, user_params) }
  let(:valid_attributes) { attributes_for(:widget) }
  let(:invalid_attributes) { attributes_for(:widget, name: nil) }
  let(:valid_headers) { Devise::JWT::TestHelpers.auth_headers({}, user) }

  let(:params) { {} }
  let(:headers) { valid_headers }

  subject do
    process method, url, params: params, headers: headers, as: :json
    response
  end

  shared_examples "successful response" do
    it { is_expected.to have_http_status :ok }
  end

  shared_examples "unauthorized response" do
    it { is_expected.to have_http_status :forbidden }
  end

  describe "GET /widgets" do
    let(:method) { :get }
    let(:url) { "/widgets" }

    before { create(:widget) }

    context 'with an admin user' do
      let(:user_params) { { is_admin: true } }

      it_behaves_like "successful response"
    end

    context 'with a standard user' do
      it_behaves_like "unauthorized response"
    end
  end
end