graphqlite icon indicating copy to clipboard operation
graphqlite copied to clipboard

Writing integration tests against graphql query

Open thamerbelfkihthamer opened this issue 5 years ago • 2 comments
trafficstars

there is any way to write integration tests aka (functional tests) against Graphql query for both Symfony & Laravel framework?

thamerbelfkihthamer avatar Apr 01 '20 11:04 thamerbelfkihthamer

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

moufmouf avatar Apr 01 '20 11:04 moufmouf

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),
        );
    }
}

oojacoboo avatar Jun 12 '22 03:06 oojacoboo