Skip to content

Commit

Permalink
Add convenient test assertions for web APIs (#2887)
Browse files Browse the repository at this point in the history
* Add convenient testing assertions

* Fix tests
  • Loading branch information
dunglas committed Jun 28, 2019
1 parent 4bfeda9 commit efa341c
Show file tree
Hide file tree
Showing 37 changed files with 1,127 additions and 51 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.php_cs
/.php_cs.cache
/.phpunit.result.cache
/build/
/composer.lock
/composer.phar
Expand Down
1 change: 0 additions & 1 deletion .php_cs.dist
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ return PhpCsFixer\Config::create()
'phpdoc_trim_consecutive_blank_line_separation' => true,
'phpdoc_var_annotation_correct_order' => true,
'return_assignment' => true,
'strict_comparison' => true,
'strict_param' => true,
'visibility_required' => [
'elements' => [
Expand Down
13 changes: 8 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
"doctrine/data-fixtures": "^1.2.2",
"doctrine/doctrine-bundle": "^1.8",
"doctrine/doctrine-cache-bundle": "^1.3.5",
"doctrine/mongodb-odm": "^2.0@beta",
"doctrine/mongodb-odm-bundle": "^4.0@beta",
"doctrine/mongodb-odm": "^2.0@rc",
"doctrine/mongodb-odm-bundle": "^4.0@rc",
"doctrine/orm": "^2.6.3",
"elasticsearch/elasticsearch": "^6.0",
"friendsofsymfony/user-bundle": "^2.2@dev",
Expand All @@ -53,7 +53,7 @@
"phpstan/phpstan-doctrine": "^0.11",
"phpstan/phpstan-phpunit": "^0.11",
"phpstan/phpstan-symfony": "^0.11",
"phpunit/phpunit": "^7.5.2",
"phpunit/phpunit": "^7.5.2 || ^8.0.0",
"psr/log": "^1.0",
"ramsey/uuid": "^3.7",
"ramsey/uuid-doctrine": "^1.4",
Expand All @@ -71,7 +71,7 @@
"symfony/expression-language": "^3.4 || ^4.0",
"symfony/finder": "^3.4 || ^4.0",
"symfony/form": "^3.4 || ^4.0",
"symfony/framework-bundle": "^4.3",
"symfony/framework-bundle": "^4.3.2",
"symfony/http-client": "^4.3",
"symfony/mercure-bundle": "*",
"symfony/messenger": "^4.3",
Expand Down Expand Up @@ -113,7 +113,10 @@
"autoload-dev": {
"psr-4": {
"ApiPlatform\\Core\\Tests\\": "tests/"
}
},
"classmap": [
"vendor/phpunit/phpunit/tests/"
]
},
"extra": {
"branch-alias": {
Expand Down
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ parameters:
- %rootDir%/../../../tests/Fixtures/app/var/cache
# The Symfony Configuration API isn't good enough to be analysed
- %rootDir%/../../../src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
# Imported code (temporary)
- %rootDir%/../../../src/Bridge/Symfony/Bundle/Test/BrowserKitAssertionsTrait.php
- %rootDir%/../../../tests/Bridge/Symfony/Bundle/Test/WebTestCaseTest.php
ignoreErrors:
# Real problems, hard to fix
- '#Parameter \#2 \$dqlPart of method Doctrine\\ORM\\QueryBuilder::add\(\) expects array\|object, string given\.#'
Expand Down
109 changes: 109 additions & 0 deletions src/Bridge/Symfony/Bundle/Test/ApiTestAssertionsTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test;

use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Constraint\ArraySubset;
use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Constraint\MatchesJsonSchema;
use PHPUnit\Framework\ExpectationFailedException;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @see \Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait
*
* @experimental
*/
trait ApiTestAssertionsTrait
{
use BrowserKitAssertionsTrait;

/**
* Asserts that the retrieved JSON contains has the specified subset.
* This method delegates to self::assertArraySubset().
*
* @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
*/
public static function assertJsonContains(array $subset, bool $checkForObjectIdentity = true, string $message = ''): void
{
static::assertArraySubset($subset, self::getHttpResponse()->toArray(false), $checkForObjectIdentity, $message);
}

/**
* Asserts that the retrieved JSON is equal to the following array.
* Both values are canonicalized before the comparision.
*/
public static function assertJsonEquals(array $json, string $message = ''): void
{
static::assertEqualsCanonicalizing($json, self::getHttpResponse()->toArray(false), $message);
}

/**
* Asserts that an array has a specified subset.
*
* Imported from dms/phpunit-arraysubset, because the original constraint has been deprecated.
*
* @copyright Sebastian Bergmann <sebastian@phpunit.de>
* @copyright Rafael Dohms <rdohms@gmail.com>
*
* @see https://github.com/sebastianbergmann/phpunit/issues/3494
*
* @param iterable $subset
* @param iterable $array
*
* @throws ExpectationFailedException
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
* @throws \Exception
*/
public static function assertArraySubset($subset, $array, bool $checkForObjectIdentity = false, string $message = ''): void
{
$constraint = new ArraySubset($subset, $checkForObjectIdentity);
static::assertThat($array, $constraint, $message);
}

/**
* @param array|string $jsonSchema
*/
public static function assertMatchesJsonSchema($jsonSchema, ?int $checkMode = null, string $message = ''): void
{
$constraint = new MatchesJsonSchema($jsonSchema, $checkMode);
static::assertThat(self::getHttpResponse()->toArray(false), $constraint, $message);
}

private static function getHttpClient(Client $newClient = null): ?Client
{
static $client;

if (0 < \func_num_args()) {
return $client = $newClient;
}

if (!$client instanceof Client) {
static::fail(sprintf('A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', __CLASS__));
}

return $client;
}

private static function getHttpResponse(): ResponseInterface
{
if (!$response = self::getHttpClient()->getResponse()) {
static::fail('A client must have an HTTP Response to make assertions. Did you forget to make an HTTP request?');
}

return $response;
}
}
15 changes: 15 additions & 0 deletions src/Bridge/Symfony/Bundle/Test/ApiTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test;

use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

Expand All @@ -25,6 +26,14 @@
*/
abstract class ApiTestCase extends KernelTestCase
{
use ApiTestAssertionsTrait;

protected function doTearDown(): void
{
parent::doTearDown();
self::getClient(null);
}

/**
* Creates a Client.
*
Expand All @@ -40,9 +49,15 @@ protected static function createClient(array $options = []): Client
*/
$client = $kernel->getContainer()->get('test.api_platform.client');
} catch (ServiceNotFoundException $e) {
if (class_exists(KernelBrowser::class)) {
throw new \LogicException('You cannot create the client used in functional tests if the "framework.test" config is not set to true.');
}
throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit".');
}

self::getHttpClient($client);
self::getClient($client->getKernelBrowser());

return $client;
}
}
170 changes: 170 additions & 0 deletions src/Bridge/Symfony/Bundle/Test/BrowserKitAssertionsTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test;

use PHPUnit\Framework\Constraint\LogicalAnd;
use PHPUnit\Framework\Constraint\LogicalNot;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint;

/**
* Copied from Symfony, to remove when https://github.com/symfony/symfony/pull/32207 will be merged.
*
* @internal
*/
trait BrowserKitAssertionsTrait
{
public static function assertResponseIsSuccessful(string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseIsSuccessful(), $message);
}

public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message);
}

public static function assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = ''): void
{
$constraint = new ResponseConstraint\ResponseIsRedirected();
if ($expectedLocation) {
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseHeaderSame('Location', $expectedLocation));
}
if ($expectedCode) {
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseStatusCodeSame($expectedCode));
}

self::assertThat(self::getResponse(), $constraint, $message);
}

public static function assertResponseHasHeader(string $headerName, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasHeader($headerName), $message);
}

public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message);
}

public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message);
}

public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message);
}

public static function assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message);
}

public static function assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message);
}

public static function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), LogicalAnd::fromConstraints(
new ResponseConstraint\ResponseHasCookie($name, $path, $domain),
new ResponseConstraint\ResponseCookieValueSame($name, $expectedValue, $path, $domain)
), $message);
}

public static function assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message);
}

public static function assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message);
}

public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), LogicalAnd::fromConstraints(
new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain),
new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain)
), $message);
}

public static function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getRequest(), new ResponseConstraint\RequestAttributeValueSame($name, $expectedValue), $message);
}

public static function assertRouteSame($expectedRoute, array $parameters = [], string $message = ''): void
{
$constraint = new ResponseConstraint\RequestAttributeValueSame('_route', $expectedRoute);
$constraints = [];
foreach ($parameters as $key => $value) {
$constraints[] = new ResponseConstraint\RequestAttributeValueSame($key, $value);
}
if ($constraints) {
$constraint = LogicalAnd::fromConstraints($constraint, ...$constraints);
}

self::assertThat(self::getRequest(), $constraint, $message);
}

private static function getClient(KernelBrowser $newClient = null): ?KernelBrowser
{
static $client;

if (0 < \func_num_args()) {
return $client = $newClient;
}

if (!$client instanceof KernelBrowser) {
static::fail(sprintf('A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', __CLASS__));
}

return $client;
}

private static function getResponse(): Response
{
if (!$response = self::getClient()->getResponse()) {
static::fail('A client must have an HTTP Response to make assertions. Did you forget to make an HTTP request?');
}

return $response;
}

private static function getRequest(): Request
{
if (!$request = self::getClient()->getRequest()) {
static::fail('A client must have an HTTP Request to make assertions. Did you forget to make an HTTP request?');
}

return $request;
}
}
Loading

0 comments on commit efa341c

Please sign in to comment.