Testing GraphQL APIs

Write calls in Playground then transform them into test scripts in Ruby

Courtney Zhan
6 min readNov 20, 2022

GraphQL is an open-source data query and manipulation language for APIs and a runtime for fulfilling queries with existing data. It stores data in a graph-like structure.

This article will go through some common GraphQL API tests.

How I write GraphQL tests:

Most GraphQL endpoints provide a convenient and user-friendly interface (called Playground) to try out queries: you can type requests and get response data on the right. You can also view the schema there.

Screenshot of GraphQL Playground for Trevor Blades’ Countries API

I follow the steps:

  1. Try out queries in GraphQL Playground.
  2. I also may do it in an HTTP graphic tool that offers more features.
  3. By then, I have a good idea of the query request I’m sending and what kind of response data I expect.
  4. Write an automated test in RSpec.
    Invoking the GraphQL endpoint code is common. The majority of the effort is to parse response data and perform assertions. Ruby is a great language for that.
  5. Run, verify and refine (typically, I do this using TestWise).

When my test suite reaches a certain size, I run them in a Continuous Testing server, such as BuildWise.

I will use Trevor Blade’s public Countries API (endpoint: https://countries.trevorblades.com/) for some of the exercises in this article.

LIST all records

GraphQL Query

query is a GraphQL command to find matching data (list). The example below gets countries and returns the code and name back.

query { 
countries {
code
name
}
}

You can run this query in the GraphQL Playground, like below:

The above retrieves the fields codeand name from the countries.

In HTTP GUI client

You can also run GraphQL queries in a GUI HTTP client, such as Paw or Postman.

CURL Equivalent

For hardcore testers, you can also do it from the command line with CURL.

curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ countries { code name } } " }' \
https://countries.trevorblades.com/

In Automated Test (Ruby)

From the above three ways, we have the request and response data. But, no testing yet.

Below is a complete automated test in RSpec.

require "httpclient"
require "net/http"
require "uri"
require "json"

describe "Graphql" do
it "List all continents" do
uri = URI.parse("https://countries.trevorblades.com/")
request = Net::HTTP::Post.new(uri)
request.content_type = "application/json"
request.body = JSON.dump({
"query" => "{ countries { code name } } ",
})
req_options = {
use_ssl: uri.scheme == "https",
}
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
expect(response.code).to eq("200") # call successful
expect(response.body).to include("Antarctica")
json_response_data = JSON.parse(response.body)
expect(json_response_data["data"]["countries"].count).to eq(250)
end
end

As you can see, after sending request and getting the data back in response we do assertions (in Ruby).

READ a specific record

GraphQL Query

query { 
continent(code: "EU") {
code
name
countries {
name
emoji
}
}
}

In RSpec Test

it "Get a particular continent" do
uri = URI.parse("https://countries.trevorblades.com/")
request = Net::HTTP::Post.new(uri)
request.content_type = "application/json"
request.body = JSON.dump({
"query" => '{ continent(code: "EU") { code name countries { name emoji } } }',
})
req_options = {
use_ssl: uri.scheme == "https",
}
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
expect(response.code).to eq("200")
expect(response.body).to include("Europe")
expect(response.body).not_to include("America")
expect(response.body).to include("Belgium")
end

CREATE a new record

Most public endpoints are read-only, which is understandable. To demonstrate mutations, we must set up somewhere we can freely mutate ourselves.

I recommend using Prisma’s Users demo on Github. Follow the instructions in the ReadMe file (linked here) to download and run the sample server on http://localhost:4000.

GraphQL Query

mutation {
signupUser(data: { name: "P Sherman", email: "psher@email.com" }) {
id
}
}
curl --request POST \
--header 'content-type: application/json' \
--url http://localhost:4000/ \
--data '{"query":"mutation {\n signupUser(data: { \n name: \"P Sherman\", email: \"psher@email.com\" }) {\n id\n }\n}","variables":{}}'

In RSpec Test

it "Sign up a new user" do
uri = URI.parse("http://localhost:4000/")
request = Net::HTTP::Post.new(uri)
request.content_type = "application/json"
request.body = JSON.dump({
"query" => "mutation {
signupUser(data: {
name: \"#{Faker::Name.name}\", email: \"#{Faker::Internet.email}\" }) {
id
}
}",
"variables" => "{}",
})
puts "request body: #{request.body}"
req_options = {
use_ssl: uri.scheme == "https",
}
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
expect(response.code).to eq("200")
puts response.body
end

UPDATE a record

For the update example, let’s update the status of a post from Published to Unpublished. I’ll use the existing sample post (id = 1):

// Result of reading Post data where id = 1
{
"data": {
"postById": {
"id": 1,
"title": "Join the Prisma Slack",
"content": "https://slack.prisma.io",
"published": true
}
}
}

GraphQL Query

There is an existing mutation to toggle the published field in the schema:

mutation TogglePublishPost($togglePublishPostId: Int!) {
togglePublishPost(id: $togglePublishPostId) {
id
published
}
}

This uses the variable togglePublishPostId to specify which post to update.

variables {
"togglePublishPostId": 1
}

In RSpec Test

it "Toggle Published status" do
uri = URI.parse("http://localhost:4000/")
request = Net::HTTP::Post.new(uri)
request.content_type = "application/json"
request.body = JSON.dump({
"query" => "mutation TogglePublishPost($togglePublishPostId: Int!) {
togglePublishPost(id: $togglePublishPostId) {
id
published
}
}",
"variables" => "{ \"togglePublishPostId\": 1 }",
})
puts "request body: #{request.body}"
req_options = {
use_ssl: uri.scheme == "https",
}
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
expect(response.code).to eq("200")
puts response.body
expect(response.body).to include("false")
end

Note: Because the above operation is Toggle, not Set, if you run the above test twice, it will pass once and fail once. Please see my previous article on how to stabilise API tests.

DELETE a record

The final operation in this tutorial is DELETE.

GraphQL Query

mutation DeletePost($deletePostId: Int!) {
deletePost(id: $deletePostId) {
id
title
}
}

Again, this uses the variable deletePostId to specify which Post to delete.

variables {
"deletePostId": 2
}

In RSpec Test

it "Delete a post" do
uri = URI.parse("http://localhost:4000/")
request = Net::HTTP::Post.new(uri)
request.content_type = "application/json"
request.body = JSON.dump({
"query" => "query PostById($postByIdId: Int) {
postById(id: $postByIdId) {
id
title
content
published
}
}}",
"variables" => "{ \"postByIdId\": 2 }",
})
puts "request body: #{request.body}"
req_options = {
use_ssl: uri.scheme == "https",
}
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
expect(response.code).to eq("200")
end

Using assertion, we can verify that the post was deleted successfully with READ. We are expecting a null value to be returned for that post Id:

request.body = JSON.dump({
"query" => "query PostById($postByIdId: Int) { postById(id: $postByIdId) { id title content published } }",
"variables" => "{ \"postByIdId\": 2 }",
})

response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
expect(response.code).to eq("200")
expect(response.body).to include("null")

--

--