Testing GraphQL APIs, Part 2: Run frequently in a Continuous Testing Server

Stabilising the tests and setting them up in a Continuous Testing server.

Table of Contents
· Stabilize Tests
1. LIST all records
2. READ one record
3. CREATE a new record
4. UPDATE a new record
5. DELETE a new record
· Run all tests in BuildWise CT Server
Run all tests locally a couple of times
BuildWise Setup

Stabilize Tests

1. LIST all records

No changes from Part 1:

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")
expect(response.body).to include("Antarctica")
json_response_data = JSON.parse(response.body)
expect(json_response_data["data"]["countries"].count).to eq(250)
end

2. READ one record

No changes, maybe. For this case, I was using a read-only end-point and assumed that the EU continent always exists. If you are querying something that changes a lot (e.g. stock inventory), the test can start to fail. We can stabilize this by invoking CREATE and then using READ on the newly created one (see UPDATE or DELETE for more).

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

3. CREATE a new record

If the ID is not provided, the service might assign a unique ID for a newly created record. Luckily, in this service, this is the case.

For the Signup User API, the email field has a unique constraint
request.body = JSON.dump({
"query" => "mutation {
signupUser(data: {
name: \"#{Faker::Name.name}\", email: \"#{Faker::Internet.email}\" }) {
id
}
}",
"variables" => "{}"
})
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

4. UPDATE a new record

We cannot just update an existing record in an automated test, because the test won’t be valid after the first run. Check out this article, “One Test Automation Scenario Interview Question that Most Candidates Failed”.

  1. Create a brand-new record
  2. Update it
  3. Verify the update
  4. Delete it
it "Toggle Published status" do
# create a new post first
post_id = create_post()

# update the newly created post
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\": #{post_id} }",
})
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("true")

# delete post
delete_post(post_id)
end

5. DELETE a new record

Like UPDATE, we shall not delete an existing record. Create a new one first.

it "Delete a post" do
post_id = create_post()
uri = URI.parse("http://localhost:4000/")
request = Net::HTTP::Post.new(uri)
request.content_type = "application/json"
request.body = JSON.dump({
"query" => "mutation DeletePost($deletePostId: Int!) {
deletePost(id: $deletePostId) {
id
title
}
}",
"variables" => "{ \"deletePostId\": #{post_id} }",
})
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")
# Assertion, read record
read_response = read_post(post_id)
expect(read_response.code).to eq("200")
expect(read_response.body).to include("null")
end

Run all tests in BuildWise CT Server

For automated end-to-end tests, if we don’t run them often in a Continuous Testing Server (not CI/CD server), those automated tests will be outdated quickly.

Run all tests locally a couple of times

Before I put the tests in a Continuous Testing Server to run as regression, I will try to run them multiple times locally first.

BuildWise Setup

You can check out this article: “Set Up a Continuous Testing Server to Run Selenium Tests in Minutes” for setting up a new BuildWise CT server instance. Or, run a BuildWise CT server in Docker.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store