idrysdale
idrysdale

Reputation: 1551

Abstracting out common setup steps in RSpec test

I'm trying to get my head around the best way to create a user token in my RSpec tests, and write them as eloquently as I can.

Below is one example for my Project class. From the spec below, you'll see I'm using DoorKeeper to keep the API endpoints secure on all actions other than show.

The problem I'm running into is how best to create that @access_token.

This works, passing all the examples, however I'm worried I'm not adhering to DRY principles. If lots of actions/contexts require an @access_token is there a way I could abstract this out somewhere to a helper of sorts?

Thanks in advance

## projects_spec.rb    

require 'spec_helper'

describe "Projects API" do

  describe "#index" do

    FactoryGirl.create(:project)

    context 'with a valid token' do

      before(:each) do 
        user = FactoryGirl.create(:user)
        authentication = FactoryGirl.create(:authentication, user: user)
        application = Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "http://app.com")
        @access_token = Doorkeeper::AccessToken.create!(:application_id => application.id, :resource_owner_id => authentication.identity.id)
      end

      it 'returns a list of projects' do
        get '/api/v1/projects', access_token: @access_token.token
        expect(response.status).to eq(200)

        # check the JSON is as we expect

      end

    end

    context 'without a token' do

      it 'responds with 401' do
        get '/api/v1/projects'
        expect(response.status).to eq(401)
      end

    end

  end

  describe "#create" do

    context 'with a valid token' do

      before(:each) do
        user = FactoryGirl.create(:user)
        authentication = FactoryGirl.create(:authentication, user: user)
        application = Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "http://app.com")
        @access_token = Doorkeeper::AccessToken.create!(:application_id => application.id, :resource_owner_id => authentication.identity.id)
      end

      context 'with required params' do 

        project_params = {
            name: "Win the lottery",
            strapline: "The best feeling in the world"
          }

        it "creates a project and responds with 201" do

          post "/api/v1/projects", :project => project_params, access_token: @access_token.token

          expect(response.status).to eq(201)

          # check the JSON is as we expect

        end

      end

      context 'without required params' do

        project_params = {
            strapline: "Stepney City Farm's pallets, woodchips and compost",
          }

        it "responds with 422 and no record created" do

          post "/api/v1/projects", :project => project_params, access_token: @access_token.token

          expect(response.status).to eq(422)

          json = JSON.parse(response.body)

          expect(json['project']['errors'].length).to eq(1)

        end

      end

    end

    context 'without a token' do

      it 'responds with 401' do
        get '/api/v1/projects'
        expect(response.status).to eq(401)
      end

    end

  end

  describe "#show" do

    it 'returns a projects' do
      project = FactoryGirl.create(:project, name: "A new project")
      get "/api/v1/projects/#{project.id}"
      expect(response.status).to eq(200)
      json = JSON.parse(response.body)
      expect(json['project']['name']).to eq(project.name)
      expect(GroupRun.last.name).to eq(project.name)
      # check the JSON is as we expect
    end

  end

end

Upvotes: 2

Views: 891

Answers (3)

idrysdale
idrysdale

Reputation: 1551

Building on Matthew's answer, with Doorkeeper, I ended up using the following:

module TokenMacros
  def generate_access_token_for(user = nil)
    user ||= FactoryGirl.create(:user)
    authentication = FactoryGirl.create(:authentication, user: user)
    application = Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "http://app.com")
    access_token = Doorkeeper::AccessToken.create!(:application_id => application.id, :resource_owner_id => authentication.identity.id)
    access_token.token
  end
end

which then allows me to call...

let(:user) { FactoryGirl.create(:user) }
let(:token) { generate_access_token_for(user) }

splendid

Upvotes: 1

Matthew Lehner
Matthew Lehner

Reputation: 4027

I have a few techniques that I use to deal with this stuff.

The first is to use plain ruby methods over let. This is just my preference, I think it adds clarity to tests. Check this for more about that: http://robots.thoughtbot.com/lets-not

And then, I have a helper method for auth stuff. I'm using Devise for authentication, so my implementation will be different than yours, but this sets the HTTP_AUTHORIZATION header for every request made to my app after calling the helper method in the tests.

module Requests
  module AuthenticationHelpers
    def basic_http_auth(user)
      credentials = ActionController::HttpAuthentication::Basic.encode_credentials(user.email,user.password)
      Rack::MockRequest::DEFAULT_ENV['HTTP_AUTHORIZATION'] = credentials
    end
  end
end

And then in the actual test, I'll do something like this:

describe "GET /api/v1/messages" do
  it 'returns an array of messages' do
    get '/api/v1/messages'
    basic_http_auth(FactoryGirl.create(:user))

    expect(response).to eq(200)
  end
end

So, if you're going to be using this across a lot of the API tests, move it into a support helper. And, if you're calling something multiple times in the same file, write a method (or put it in a let call) to DRY your your tests.

Upvotes: 1

Peter Alfvin
Peter Alfvin

Reputation: 29439

The easiest thing to do to avoid the duplication would be to define the token in the highest level describe so that it's available for all the examples in your spec.

In addition, to avoid any performance issue for the examples that don't depend on it, you can use let rather than before to define the token since let is lazily evaluated.

What you'd have then, is the following:

let(:access_token) do
  user = FactoryGirl.create(:user)
  authentication = FactoryGirl.create(:authentication, user: user)
  application = Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "http://app.com")
  Doorkeeper::AccessToken.create!(:application_id => application.id, :resource_owner_id => authentication.identity.id)
  end

You'd need to also change your references to this token from @access_token to access_token, since you're defining a method and not an instance variable.

Upvotes: 0

Related Questions