Skip to content

Writing integration tests against graphql query #252

Description

@thamerbelfkihthamer

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

Activity

  1. moufmouf commented on Apr 1, 2020

    @moufmouf
    Member

    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

  2. added
    documentationRelated to errors, omissions and improvements to docs
    on Mar 29, 2021
  3. pinned this issue on Jun 12, 2022
  4. oojacoboo commented on Jun 12, 2022

    @oojacoboo
    Collaborator

    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 <jacob@rentpost.com>
     */
    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),
            );
        }
    }
  5. changed the title [-]how to writes integration tests against graphql query[/-] [+]Writing integration tests against graphql query[/+] on Jun 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationRelated to errors, omissions and improvements to docs

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions