Testing GraphQL APIs
Write calls in Playground then transform them into test scripts in Ruby
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.
I follow the steps:
- Try out queries in GraphQL Playground.
- I also may do it in an HTTP graphic tool that offers more features.
- By then, I have a good idea of the query request I’m sending and what kind of response data I expect.
- 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. - 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 code
and 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
, notSet
, 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")