Skip to content

Commit efa341c

Browse files
authored
Add convenient test assertions for web APIs (#2887)
* Add convenient testing assertions * Fix tests
1 parent 4bfeda9 commit efa341c

37 files changed

+1127
-51
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/.php_cs
22
/.php_cs.cache
3+
/.phpunit.result.cache
34
/build/
45
/composer.lock
56
/composer.phar

.php_cs.dist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ return PhpCsFixer\Config::create()
9898
'phpdoc_trim_consecutive_blank_line_separation' => true,
9999
'phpdoc_var_annotation_correct_order' => true,
100100
'return_assignment' => true,
101-
'strict_comparison' => true,
102101
'strict_param' => true,
103102
'visibility_required' => [
104103
'elements' => [

composer.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
"doctrine/data-fixtures": "^1.2.2",
3737
"doctrine/doctrine-bundle": "^1.8",
3838
"doctrine/doctrine-cache-bundle": "^1.3.5",
39-
"doctrine/mongodb-odm": "^2.0@beta",
40-
"doctrine/mongodb-odm-bundle": "^4.0@beta",
39+
"doctrine/mongodb-odm": "^2.0@rc",
40+
"doctrine/mongodb-odm-bundle": "^4.0@rc",
4141
"doctrine/orm": "^2.6.3",
4242
"elasticsearch/elasticsearch": "^6.0",
4343
"friendsofsymfony/user-bundle": "^2.2@dev",
@@ -53,7 +53,7 @@
5353
"phpstan/phpstan-doctrine": "^0.11",
5454
"phpstan/phpstan-phpunit": "^0.11",
5555
"phpstan/phpstan-symfony": "^0.11",
56-
"phpunit/phpunit": "^7.5.2",
56+
"phpunit/phpunit": "^7.5.2 || ^8.0.0",
5757
"psr/log": "^1.0",
5858
"ramsey/uuid": "^3.7",
5959
"ramsey/uuid-doctrine": "^1.4",
@@ -71,7 +71,7 @@
7171
"symfony/expression-language": "^3.4 || ^4.0",
7272
"symfony/finder": "^3.4 || ^4.0",
7373
"symfony/form": "^3.4 || ^4.0",
74-
"symfony/framework-bundle": "^4.3",
74+
"symfony/framework-bundle": "^4.3.2",
7575
"symfony/http-client": "^4.3",
7676
"symfony/mercure-bundle": "*",
7777
"symfony/messenger": "^4.3",
@@ -113,7 +113,10 @@
113113
"autoload-dev": {
114114
"psr-4": {
115115
"ApiPlatform\\Core\\Tests\\": "tests/"
116-
}
116+
},
117+
"classmap": [
118+
"vendor/phpunit/phpunit/tests/"
119+
]
117120
},
118121
"extra": {
119122
"branch-alias": {

phpstan.neon.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ parameters:
1616
- %rootDir%/../../../tests/Fixtures/app/var/cache
1717
# The Symfony Configuration API isn't good enough to be analysed
1818
- %rootDir%/../../../src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
19+
# Imported code (temporary)
20+
- %rootDir%/../../../src/Bridge/Symfony/Bundle/Test/BrowserKitAssertionsTrait.php
21+
- %rootDir%/../../../tests/Bridge/Symfony/Bundle/Test/WebTestCaseTest.php
1922
ignoreErrors:
2023
# Real problems, hard to fix
2124
- '#Parameter \#2 \$dqlPart of method Doctrine\\ORM\\QueryBuilder::add\(\) expects array\|object, string given\.#'
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test;
15+
16+
use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Constraint\ArraySubset;
17+
use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Constraint\MatchesJsonSchema;
18+
use PHPUnit\Framework\ExpectationFailedException;
19+
use Symfony\Contracts\HttpClient\ResponseInterface;
20+
21+
/**
22+
* @see \Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait
23+
*
24+
* @experimental
25+
*/
26+
trait ApiTestAssertionsTrait
27+
{
28+
use BrowserKitAssertionsTrait;
29+
30+
/**
31+
* Asserts that the retrieved JSON contains has the specified subset.
32+
* This method delegates to self::assertArraySubset().
33+
*
34+
* @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
35+
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
36+
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
37+
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
38+
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
39+
*/
40+
public static function assertJsonContains(array $subset, bool $checkForObjectIdentity = true, string $message = ''): void
41+
{
42+
static::assertArraySubset($subset, self::getHttpResponse()->toArray(false), $checkForObjectIdentity, $message);
43+
}
44+
45+
/**
46+
* Asserts that the retrieved JSON is equal to the following array.
47+
* Both values are canonicalized before the comparision.
48+
*/
49+
public static function assertJsonEquals(array $json, string $message = ''): void
50+
{
51+
static::assertEqualsCanonicalizing($json, self::getHttpResponse()->toArray(false), $message);
52+
}
53+
54+
/**
55+
* Asserts that an array has a specified subset.
56+
*
57+
* Imported from dms/phpunit-arraysubset, because the original constraint has been deprecated.
58+
*
59+
* @copyright Sebastian Bergmann <[email protected]>
60+
* @copyright Rafael Dohms <[email protected]>
61+
*
62+
* @see https://github.com/sebastianbergmann/phpunit/issues/3494
63+
*
64+
* @param iterable $subset
65+
* @param iterable $array
66+
*
67+
* @throws ExpectationFailedException
68+
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
69+
* @throws \Exception
70+
*/
71+
public static function assertArraySubset($subset, $array, bool $checkForObjectIdentity = false, string $message = ''): void
72+
{
73+
$constraint = new ArraySubset($subset, $checkForObjectIdentity);
74+
static::assertThat($array, $constraint, $message);
75+
}
76+
77+
/**
78+
* @param array|string $jsonSchema
79+
*/
80+
public static function assertMatchesJsonSchema($jsonSchema, ?int $checkMode = null, string $message = ''): void
81+
{
82+
$constraint = new MatchesJsonSchema($jsonSchema, $checkMode);
83+
static::assertThat(self::getHttpResponse()->toArray(false), $constraint, $message);
84+
}
85+
86+
private static function getHttpClient(Client $newClient = null): ?Client
87+
{
88+
static $client;
89+
90+
if (0 < \func_num_args()) {
91+
return $client = $newClient;
92+
}
93+
94+
if (!$client instanceof Client) {
95+
static::fail(sprintf('A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', __CLASS__));
96+
}
97+
98+
return $client;
99+
}
100+
101+
private static function getHttpResponse(): ResponseInterface
102+
{
103+
if (!$response = self::getHttpClient()->getResponse()) {
104+
static::fail('A client must have an HTTP Response to make assertions. Did you forget to make an HTTP request?');
105+
}
106+
107+
return $response;
108+
}
109+
}

src/Bridge/Symfony/Bundle/Test/ApiTestCase.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

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

16+
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
1617
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
1718
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
1819

@@ -25,6 +26,14 @@
2526
*/
2627
abstract class ApiTestCase extends KernelTestCase
2728
{
29+
use ApiTestAssertionsTrait;
30+
31+
protected function doTearDown(): void
32+
{
33+
parent::doTearDown();
34+
self::getClient(null);
35+
}
36+
2837
/**
2938
* Creates a Client.
3039
*
@@ -40,9 +49,15 @@ protected static function createClient(array $options = []): Client
4049
*/
4150
$client = $kernel->getContainer()->get('test.api_platform.client');
4251
} catch (ServiceNotFoundException $e) {
52+
if (class_exists(KernelBrowser::class)) {
53+
throw new \LogicException('You cannot create the client used in functional tests if the "framework.test" config is not set to true.');
54+
}
4355
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".');
4456
}
4557

58+
self::getHttpClient($client);
59+
self::getClient($client->getKernelBrowser());
60+
4661
return $client;
4762
}
4863
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
/*
15+
* This file is part of the Symfony package.
16+
*
17+
* (c) Fabien Potencier <[email protected]>
18+
*
19+
* For the full copyright and license information, please view the LICENSE
20+
* file that was distributed with this source code.
21+
*/
22+
23+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test;
24+
25+
use PHPUnit\Framework\Constraint\LogicalAnd;
26+
use PHPUnit\Framework\Constraint\LogicalNot;
27+
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
28+
use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint;
29+
use Symfony\Component\HttpFoundation\Request;
30+
use Symfony\Component\HttpFoundation\Response;
31+
use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint;
32+
33+
/**
34+
* Copied from Symfony, to remove when https://github.com/symfony/symfony/pull/32207 will be merged.
35+
*
36+
* @internal
37+
*/
38+
trait BrowserKitAssertionsTrait
39+
{
40+
public static function assertResponseIsSuccessful(string $message = ''): void
41+
{
42+
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseIsSuccessful(), $message);
43+
}
44+
45+
public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void
46+
{
47+
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message);
48+
}
49+
50+
public static function assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = ''): void
51+
{
52+
$constraint = new ResponseConstraint\ResponseIsRedirected();
53+
if ($expectedLocation) {
54+
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseHeaderSame('Location', $expectedLocation));
55+
}
56+
if ($expectedCode) {
57+
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseStatusCodeSame($expectedCode));
58+
}
59+
60+
self::assertThat(self::getResponse(), $constraint, $message);
61+
}
62+
63+
public static function assertResponseHasHeader(string $headerName, string $message = ''): void
64+
{
65+
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasHeader($headerName), $message);
66+
}
67+
68+
public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void
69+
{
70+
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message);
71+
}
72+
73+
public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void
74+
{
75+
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message);
76+
}
77+
78+
public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void
79+
{
80+
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message);
81+
}
82+
83+
public static function assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
84+
{
85+
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message);
86+
}
87+
88+
public static function assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
89+
{
90+
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message);
91+
}
92+
93+
public static function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = ''): void
94+
{
95+
self::assertThat(self::getResponse(), LogicalAnd::fromConstraints(
96+
new ResponseConstraint\ResponseHasCookie($name, $path, $domain),
97+
new ResponseConstraint\ResponseCookieValueSame($name, $expectedValue, $path, $domain)
98+
), $message);
99+
}
100+
101+
public static function assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
102+
{
103+
self::assertThat(self::getClient(), new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message);
104+
}
105+
106+
public static function assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
107+
{
108+
self::assertThat(self::getClient(), new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message);
109+
}
110+
111+
public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', string $domain = null, string $message = ''): void
112+
{
113+
self::assertThat(self::getClient(), LogicalAnd::fromConstraints(
114+
new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain),
115+
new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain)
116+
), $message);
117+
}
118+
119+
public static function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void
120+
{
121+
self::assertThat(self::getRequest(), new ResponseConstraint\RequestAttributeValueSame($name, $expectedValue), $message);
122+
}
123+
124+
public static function assertRouteSame($expectedRoute, array $parameters = [], string $message = ''): void
125+
{
126+
$constraint = new ResponseConstraint\RequestAttributeValueSame('_route', $expectedRoute);
127+
$constraints = [];
128+
foreach ($parameters as $key => $value) {
129+
$constraints[] = new ResponseConstraint\RequestAttributeValueSame($key, $value);
130+
}
131+
if ($constraints) {
132+
$constraint = LogicalAnd::fromConstraints($constraint, ...$constraints);
133+
}
134+
135+
self::assertThat(self::getRequest(), $constraint, $message);
136+
}
137+
138+
private static function getClient(KernelBrowser $newClient = null): ?KernelBrowser
139+
{
140+
static $client;
141+
142+
if (0 < \func_num_args()) {
143+
return $client = $newClient;
144+
}
145+
146+
if (!$client instanceof KernelBrowser) {
147+
static::fail(sprintf('A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', __CLASS__));
148+
}
149+
150+
return $client;
151+
}
152+
153+
private static function getResponse(): Response
154+
{
155+
if (!$response = self::getClient()->getResponse()) {
156+
static::fail('A client must have an HTTP Response to make assertions. Did you forget to make an HTTP request?');
157+
}
158+
159+
return $response;
160+
}
161+
162+
private static function getRequest(): Request
163+
{
164+
if (!$request = self::getClient()->getRequest()) {
165+
static::fail('A client must have an HTTP Request to make assertions. Did you forget to make an HTTP request?');
166+
}
167+
168+
return $request;
169+
}
170+
}

0 commit comments

Comments
 (0)