Julien Camblan
Posted on July 3, 2020
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?
Posted on July 3, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.