Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Writing integration tests against graphql query #252

Open
thamerbelfkihthamer opened this issue Apr 1, 2020 · 2 comments
Open

Writing integration tests against graphql query #252

thamerbelfkihthamer opened this issue Apr 1, 2020 · 2 comments
Labels
documentation Related to errors, omissions and improvements to docs

Comments

@thamerbelfkihthamer
Copy link

thamerbelfkihthamer commented Apr 1, 2020

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

@moufmouf
Copy link
Member

moufmouf commented Apr 1, 2020

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

@oojacoboo oojacoboo added the documentation Related to errors, omissions and improvements to docs label Mar 29, 2021
@oojacoboo oojacoboo pinned this issue Jun 12, 2022
@oojacoboo
Copy link
Collaborator

oojacoboo commented Jun 12, 2022

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

@oojacoboo oojacoboo changed the title how to writes integration tests against graphql query Writing integration tests against graphql query Jun 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Related to errors, omissions and improvements to docs
Projects
None yet
Development

No branches or pull requests

3 participants