One of the most important pillars of developing and maintaining an API project is having a functional test suite that ensures the quality of request responses. In addition to quality assurance, functional tests also help the developer a great deal by simulating a client that can assert expectations, letting the developer know when objectives have been met. While most APIs still use a REST-based architecture, GraphQL has emerged as a popular choice due to how well it pairs with creating a rich client app. GraphQL gives the app developer tremendous control to request only what data is needed and nothing more, as well as fewer requests per page.
When it comes to functional testing, patterns for REST-based endpoints have been well established, but less so for GraphQL. GraphQL requests are different from REST endpoints in a few ways. GraphQL shares one endpoint, differing only in the payload that is sent to this endpoint. Not only is the same endpoint used for all requests and mutations, but the same JSON argument as well.
Implementation
I will be using Symfony, PHPUnit, and API-Platform as the frameworks in this article, but the same “adjustments” should be possible in any framework.
These frameworks come with some nice helpers to get tests up and running fast. A ‘client’ is easily initiated to make requests with, and a suite of assertions is available for testing the results of each request.
The Query Request
My tests all extend a subclass where I put the following method.
The GraphQL request is simply standardizing the structure of each request.
<?php
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
class MyApiTestCase extends ApiTestCase {
protected function executeGQL($query, $variables = []): Response {
$client = self::createClient();
$response = $client->request('POST', '/api/graphql', [
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'query' => $query,
'variables' => $variables
]
]);
return $response;
}
}
We use this method in our subclass like…
$gql = <<<GQL
query(\$id: ID!) {
user(id: \$id) {
id
username
email
location {
name
}
}
}
GQL;
$input = [
"id" => "/api/users/1",
];
$response = $this->executeGQL($gql, $input);
For the query itself, we make use of PHP’s HereDoc
syntax. This is a big win because it makes query composition readable. GraphQL itself doesn’t need to be arranged with the right amount of indentation, etc. But it could get pretty messy if we issued queries as one big line of text.
Also, note that we escape the $
characters used as arguments.
The input array should resemble the GraphQL schema for the query.
A Mutation Request
A mutation request is made the same way. Unlike REST we are not issuing these requests as different types, everything is a POST
request.
$gql = <<<GQL
mutation createUser(\$input: createUserInput!) {
createUser(input: \$input) {
user {
id
username
email
}
}
}
GQL;
$input = [
"input" => [
"username" => "phpunit",
"password" => "test",
"roles" => ['ROLE_USER'],
"email" => "phpunit@example.com",
"firstName" => "PHP",
"lastName" => "Unit",
"active" => TRUE,
],
];
$response = $this->executeGQL($gql, $input);
The Response
The response for GraphQL requests using API-Platform is in JSON, just like REST requests. But since we are expecting a heavily nested array, it might be worth creating some custom helpers to ease assertions.
Here is one (of many) I created as an example.
protected function assertValueAtPath($path, $response, $value, $message = ''): Void {
$data = $this->getDataForResponse($response);
$pathArray = explode('.', $path);
$parent = $data;
for ($i=0; $i < count($pathArray); $i++) {
$current = $pathArray[$i];
if ($pathArray[$i] == "0") {
$this->assertIsArray($parent, $message . "Response data is not an array at path [$current]");
$this->assertArrayHasKey($pathArray[$i], $parent);
$parent = $parent[$pathArray[$i]];
}
else {
$this->assertIsObject($parent, $message . "Response data is not an object at path [$current]");
$this->assertObjectHasAttribute($pathArray[$i], $parent);
$parent = $parent->{$pathArray[$i]};
}
};
$this->assertEquals($value, $parent, $message . " – Property value matches at path [$path]");
}
Using it like so...
$response = $this->executeGQL($gql, $input);
$this->assertValueAtPath('data.createUser.user.username', $response, 'phpunit');
Hopefully, this gets you started!