graphqlite
graphqlite copied to clipboard
Writing integration tests against graphql query
trafficstars
there is any way to write integration tests aka (functional tests) against Graphql query for both Symfony & Laravel framework?
This is something we need to document (and make easier) For the record, I'm adding the documentation to Lighthouse: https://lighthouse-php.com/4.11/testing/phpunit.html
We might consider providing an abstract phpunit test class or trait for this. We've created a way of executing operations for tests using the following PHPUnit test class:
<?php
declare(strict_types = 1);
namespace Test\Integration;
use Acme\Widget\Request\Handler as RequestHandler;
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
/**
* Base GraphQL integration test class
*
* @author Jacob Thomason <[email protected]>
*/
abstract class AbstractGraphQLTest extends TestCase
{
private RequestHandler $requestHandler;
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
$this->requestHandler = $this->getContainer()->get(RequestHandler::class);
}
/**
* Executes a GraphQL operation request
*
* Consider executing this through another process, or even sending it through HTTP. As
* it is now, we're doing a lot of fancy stuff with the EntityManager to make it work.
*
* It is working quite well though and stable, just that the entity manager has to have it's
* state carefully managed. Moving to another actual HTTP request would make it very difficult
* to get stack traces and introduce Nginx overhead. A separate PHP process might be the best
* solution, if we're able to get the stack-traces.
*
* @param mixed[] $variables
* @param UploadedFileInterface[] $uploadedFiles
*/
protected function operation(
string $operation,
string $operationType,
array $variables = [],
array $uploadedFiles = [],
array $map = [],
): ResponseInterface
{
// We must clear the entity manager since we're often creating entities/records that we then
// want to test with the GraphQL layer. Since these entities are cached, if it's not cleared,
// it will always find them using the cache. In many cases, especially when testing the
// multi-tenancy functionality, we're executing these requests as a different role and therefore
// the cache shouldn't be used and a fresh query should be done.
$this->getContainer()->get('entity_manager_registry')->getManager()->clear();
$contentType = 'application/json';
$parsedBody = [
'query' => $operation,
'variables' => $variables,
'operationName' => $this->getName(),
];
// With uploads we have to use the multipart content-type, but also the request body differs.
// @see https://github.com/jaydenseric/graphql-multipart-request-spec
// Also we have to json_encode these property values for the GraphQL upload lib we're using.
// The inconsistency here really sucks and causes a number of weird conditional logic.
if ($uploadedFiles) {
$contentType = 'multipart/form-data; boundary=----WebKitFormBoundarySl4GaqVa1r8GtAbn';
$parsedBody = [
'operations' => json_encode($parsedBody),
'map' => json_encode($map),
];
}
$request = (new ServerRequest([], [], '/graphql', 'POST'))
->withHeader('content-type', $contentType)
->withHeader('Authorization', 'Bearer ' . /*get your auth key/token*/)
->withParsedBody($parsedBody)
->withUploadedFiles($uploadedFiles);
return $this->requestHandler->handle($request);
}
/**
* Execute a GraphQL query
*
* @param mixed $params,...
*/
protected function query(string $query, ...$params): ResponseInterface
{
$query = sprintf('query %s {%s}', $this->getName(), sprintf($query, ...$params));
return $this->operation($query, 'query');
}
/**
* Execute a GraphQL mutation
*
* @param mixed $params,...
*/
protected function mutation(string $mutation, ...$params): ResponseInterface
{
$mutation = sprintf('mutation %s {%s}', $this->getName(), sprintf($mutation, ...$params));
return $this->operation($mutation, 'mutation');
}
/**
* Execute a GraphQL mutation with uploaded files
*
* @param mixed $params,...
*/
protected function mutationWithUpload(
string $mutation,
UploadedFileInterface $uploadedFile,
...$params
): ResponseInterface
{
$files = [1 => $uploadedFile];
$map = [1 => ['variables.file']];
$variables = ['file' => null];
$mutation = sprintf(
'mutation %s($file: Upload!) {%s}',
$this->getName(),
sprintf($mutation, ...$params),
);
return $this->operation($mutation, 'mutation', $variables, $files, $map);
}
/**
* Gets the response data array from the response object
*/
protected function getResponseData(ResponseInterface $response): array
{
$data = [];
$responseBody = $response->getBody()->__toString();
$responseCode = $response->getStatusCode();
if ($responseBody) {
$responseContents = json_decode($responseBody, true);
if (!$responseContents) {
throw new Exception(
'Unable to get a valid response body.
Response: (' . $responseCode . ') ' . $responseBody,
);
}
if (!isset($responseContents['data'])) {
throw new Exception(
'Response body does not include a "data" key.
Response: (' . $responseCode . ') ' . $responseBody,
);
}
$data = $responseContents['data'];
}
return $data;
}
/**
* Asserts that the response is as expected
*
* @param string[] $expected
*/
protected function assertResponseDataEquals(ResponseInterface $response, array $expected): void
{
$this->assertEquals(
$expected,
$this->getResponseData($response),
$response->getBody()->getContents(),
);
}
/**
* Asserts that the response contains the number of expected results
*/
protected function assertResponseCountEquals(
ResponseInterface $response,
string $field,
int $expectedCount
): void
{
$this->assertEquals($expectedCount, count($this->getResponseData($response)[$field]));
}
/**
* Asserts that the response has results
*/
protected function assertResponseHasResults(ResponseInterface $response): void
{
$this->assertNotEmpty($this->getResponseData($response));
}
/**
* Asserts that the response does not have any errors
*/
protected function assertResponseHasNoErrors(ResponseInterface $response): void
{
$responseContents = json_decode($response->getBody()->__toString(), true);
$errorMessage = isset($responseContents['errors'])
? $responseContents['errors'][0]['message']
: '';
$errorMessage .= isset($responseContents['errors'][0]['extensions']['fields'])
&& count($responseContents['errors'][0]['extensions']['fields']) > 0
? ' (' . implode(', ', $responseContents['errors'][0]['extensions']['fields']) . ')'
: '';
try {
$this->assertEmpty($errorMessage, $errorMessage);
} catch (ExpectationFailedException $e) {
throw new ExpectationFailedException(
'Failed response (' . $response->getStatusCode() . '): ' . $errorMessage,
null,
$e,
);
}
}
/**
* Asserts the HTTP status code from the response
*/
protected function assertResponseCode(ResponseInterface $response, int $expectedCode, string $message = ''): void
{
$statusCode = $response->getStatusCode();
$message = $message ?: $response->getBody()->getContents();
$this->assertEquals(
$expectedCode,
$statusCode,
\sprintf('HTTP status code of "%s" is expected "%s". %s', $statusCode, $expectedCode, $message),
);
}
}