Save time on graphql-ruby tests with basic helpers

camblan

Julien Camblan

Posted on July 3, 2020

Save time on graphql-ruby tests with basic helpers

APIs built with graphql-ruby are easy to test because GraphQL is structurally designed in lighter class than a standard Rails REST API.
We can have one spec for each mutation, one for each resolver, type, etc. It's nice because you don't have to overthink anything in order to test in isolation.

Nevertheless GraphQL queries calls are a bit more verbose to declare and spec files can quickly become messy when you need to test a query with different arguments, variables, etc.

I made some helpers to help me save time when writing my tests, and I thought it could be useful to other graphql-ruby newcomers so here they are.

# /spec/support/graphql_helpers.rb

module Requests
  module GraphqlHelpers
    # Nothing fancy here, I just parse the body response before using it below
    def json
      JSON.parse(response.body)
    end

    # This method launch the graphql request with the `query` and `variables` variables
    # by default. It's convenient when you don't have anything really complicated to test.
    # And when you need to test multiple requests, you can call it with arguments.
    # AccessToken is also generated in here rather than in specs files like I did for a long time
    def do_graphql_request(user: nil, qry: nil, var: nil)
      # create is a FactoryBot method
      token = create(:access_token, resource_owner_id: user&.id || current_user&.id)
      post '/graphql', params: {
        query: qry || query,
        variables: var || variables
      }, headers: {
        Authorization: "Bearer #{token.token}"
      }
    end

    # by default, variables is empty indeed
    def variables
      {}
    end

    # I managed to format every errors on my API with the same type
    # so I can use shared_examples for some common test cases
    RSpec.shared_examples 'unauthorized user' do
      it 'return unauthorized error message' do
        expect(errors.pluck('key')).to include('unauthorized')
      end
    end

    # some querys or mutation might be accessible by unauthenticated visitors
    # like registration for example. I need to handle this case.
    def current_user
      nil
    end

    # All my mutations are returning the same payload:
    #
    # field :object, Types::ObjectType, null: true
    # field :errors, [Types::MutationErrorType], null: true
    #
    # and MutationErrorType is defined as follows
    #
    # class Types::MutationErrorType < Types::BaseObject
    #   field :key, String, null: true
    #   field :message, String, null: true
    # end
    def mutation_errors
      json.dig('data', described_class.graphql_name.camelize(:lower), 'errors')
    end
  end
end

This file need to be included in your /spec/rails_helper.rb:

# /spec/rails_helper.rb

# ...

Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f }

#  ...

RSpec.configure do |config|
  config.include Requests::GraphqlHelpers, type: :request

  # ...

end

As a result, spec files stay pretty clean:

# /spec/graphql/mutations/create_job_spec.rb

RSpec.describe Mutations::CreateJob, type: :request do
  let(:current_user) { create(:user) }
  let(:company) { create(:company, user: current_user) }

  let(:result) { json.dig('data', 'createJob', 'job') }

  context 'when user is company owner' do
    before do
      do_graphql_request
    end

    it 'creates new job' do
      expect(result.dig('uuid')).to be_truthy
    end

    it 'links new job to company' do
      expect(result.dig('company', 'uuid')).to eq(company.uuid)
    end

    it 'set new job state to draft' do
      expect(result.dig('state')).to eq('draft')
    end
  end

  context 'when user is not company owner' do
    before do
      do_graphql_request(user: create(:user))
    end

    it_behaves_like 'unauthorized user'
  end

  def query
    <<-GRAPHQL
      mutation createJob($input: CreateJobInput!) {
        createJob(input: $input) {
          job {
            uuid
            title
            description
            state
            city
            company {
              uuid
              name
            }
          }
          errors {
            key
            message
          }
        }
      }
    GRAPHQL
  end

  def variables
    {
      input: {
        companyId: company.uuid,
        title: FFaker::LoremFR.word,
        description: FFaker::LoremFR.paragraph,
        city: FFaker::Address.city
      }
    }
  end
end

Hope it'll help someone!
Do you have tips to share for better GraphQL testing?

💖 💪 🙅 🚩
camblan
Julien Camblan

Posted on July 3, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related