diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index ac3d971898..3d19141469 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 4.3.0 ----- + * added `WebTestAssertions` trait (included by default in `WebTestCase`) * renamed `Client` to `KernelBrowser` * Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will be mandatory in 5.0. diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php index 666831b041..e794b2b61d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -23,7 +23,7 @@ use Symfony\Contracts\Service\ResetInterface; */ abstract class KernelTestCase extends TestCase { - use KernelShutdownOnTearDownTrait; + use TestCaseSetUpTearDownTrait; protected static $class; @@ -37,6 +37,11 @@ abstract class KernelTestCase extends TestCase */ protected static $container; + protected function doTearDown(): void + { + static::ensureKernelShutdown(); + } + /** * @return string The Kernel class name * diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelShutdownOnTearDownTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/TestCaseSetUpTearDownTrait.php similarity index 53% rename from src/Symfony/Bundle/FrameworkBundle/Test/KernelShutdownOnTearDownTrait.php rename to src/Symfony/Bundle/FrameworkBundle/Test/TestCaseSetUpTearDownTrait.php index bc23a39cfa..8fc0997913 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelShutdownOnTearDownTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/TestCaseSetUpTearDownTrait.php @@ -13,31 +13,60 @@ namespace Symfony\Bundle\FrameworkBundle\Test; use PHPUnit\Framework\TestCase; -// Auto-adapt to PHPUnit 8 that added a `void` return-type to the tearDown method +// Auto-adapt to PHPUnit 8 that added a `void` return-type to the setUp/tearDown methods if ((new \ReflectionMethod(TestCase::class, 'tearDown'))->hasReturnType()) { /** * @internal */ - trait KernelShutdownOnTearDownTrait + trait TestCaseSetUpTearDownTrait { + private function doSetUp(): void + { + } + + private function doTearDown(): void + { + } + + protected function setUp(): void + { + $this->doSetUp(); + } + protected function tearDown(): void { - static::ensureKernelShutdown(); + $this->doTearDown(); } } } else { /** * @internal */ - trait KernelShutdownOnTearDownTrait + trait TestCaseSetUpTearDownTrait { + private function doSetUp(): void + { + } + + private function doTearDown(): void + { + } + + /** + * @return void + */ + protected function setUp() + { + $this->doSetUp(); + } + /** * @return void */ protected function tearDown() { - static::ensureKernelShutdown(); + $this->doTearDown(); } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertions.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertions.php new file mode 100644 index 0000000000..25eac6fe0a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertions.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\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\DomCrawler\Crawler; +use Symfony\Component\DomCrawler\Test\Constraint as DomCrawlerConstraint; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint; + +/** + * Ideas borrowed from Laravel Dusk's assertions. + * + * @see https://laravel.com/docs/5.7/dusk#available-assertions + */ +trait WebTestAssertions +{ + public static function assertResponseIsSuccessful(string $message = ''): void + { + self::assertThat(static::getResponse(), new ResponseConstraint\ResponseIsSuccessful(), $message); + } + + public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void + { + self::assertThat(static::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(static::getResponse(), $constraint, $message); + } + + public static function assertResponseHasHeader(string $headerName, string $message = ''): void + { + self::assertThat(static::getResponse(), new ResponseConstraint\ResponseHasHeader($headerName), $message); + } + + public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void + { + self::assertThat(static::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message); + } + + public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat(static::getResponse(), new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message); + } + + public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat(static::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(static::getResponse(), new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message); + } + + public static function assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void + { + self::assertThat(static::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(static::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(static::getClient(), new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message); + } + + public static function assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void + { + self::assertThat(static::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(static::getClient(), LogicalAnd::fromConstraints( + new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), + new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain) + ), $message); + } + + public static function assertSelectorExists(string $selector, string $message = ''): void + { + self::assertThat(static::getCrawler(), new DomCrawlerConstraint\CrawlerSelectorExists($selector), $message); + } + + public static function assertSelectorNotExists(string $selector, string $message = ''): void + { + self::assertThat(static::getCrawler(), new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorExists($selector)), $message); + } + + public static function assertSelectorTextContains(string $selector, string $text, string $message = ''): void + { + self::assertThat(static::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text) + ), $message); + } + + public static function assertSelectorTextSame(string $selector, string $text, string $message = ''): void + { + self::assertThat(static::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new DomCrawlerConstraint\CrawlerSelectorTextSame($selector, $text) + ), $message); + } + + public static function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void + { + self::assertThat(static::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text)) + ), $message); + } + + public static function assertPageTitleSame(string $expectedTitle, string $message = ''): void + { + self::assertSelectorTextSame('title', $expectedTitle, $message); + } + + public static function assertPageTitleContains(string $expectedTitle, string $message = ''): void + { + self::assertSelectorTextContains('title', $expectedTitle, $message); + } + + public static function assertInputValueSame(string $fieldName, string $expectedValue, string $message = ''): void + { + self::assertThat(static::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue) + ), $message); + } + + public static function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void + { + self::assertThat(static::getCrawler(), LogicalAnd::fromConstraints( + new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue)) + ), $message); + } + + public static function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void + { + self::assertThat(static::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(static::getRequest(), $constraint, $message); + } + + private static function getClient(): KernelBrowser + { + if (!static::$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 static::$client; + } + + private static function getCrawler(): Crawler + { + if (!$crawler = static::getClient()->getCrawler()) { + static::fail('A client must have a crawler to make assertions. Did you forget to make an HTTP request?'); + } + + return $crawler; + } + + private static function getResponse(): Response + { + if (!$response = static::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 = static::getClient()->getRequest()) { + static::fail('A client must have an HTTP Request to make assertions. Did you forget to make an HTTP request?'); + } + + return $request; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php index 5334d58361..aa2b12ea29 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php @@ -21,6 +21,18 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; */ abstract class WebTestCase extends KernelTestCase { + use WebTestAssertions; + + /** @var Client|null */ + protected static $client; + + protected function doTearDown(): void + { + parent::doTearDown(); + + static::$client = null; + } + /** * Creates a KernelBrowser. * @@ -44,6 +56,6 @@ abstract class WebTestCase extends KernelTestCase $client->setServerParameters($server); - return $client; + return static::$client = $client; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php new file mode 100644 index 0000000000..92f200be1f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php @@ -0,0 +1,288 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Test; + +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Bundle\FrameworkBundle\Test\WebTestAssertions; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\BrowserKit\Cookie; +use Symfony\Component\BrowserKit\CookieJar; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpFoundation\Cookie as HttpFoundationCookie; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +class WebTestCaseTest extends TestCase +{ + public function testAssertResponseIsSuccessful() + { + $this->getResponseTester(new Response())->assertResponseIsSuccessful(); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage("Failed asserting that the Response is successful.\nHTTP/1.0 404 Not Found"); + $this->getResponseTester(new Response('', 404))->assertResponseIsSuccessful(); + } + + public function testAssertResponseStatusCodeSame() + { + $this->getResponseTester(new Response())->assertResponseStatusCodeSame(200); + $this->getResponseTester(new Response('', 404))->assertResponseStatusCodeSame(404); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage("Failed asserting that the Response status code is 200.\nHTTP/1.0 404 Not Found"); + $this->getResponseTester(new Response('', 404))->assertResponseStatusCodeSame(200); + } + + public function testAssertResponseRedirects() + { + $this->getResponseTester(new Response('', 301))->assertResponseRedirects(); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage("Failed asserting that the Response is redirected.\nHTTP/1.0 200 OK"); + $this->getResponseTester(new Response())->assertResponseRedirects(); + } + + public function testAssertResponseRedirectsWithLocation() + { + $this->getResponseTester(new Response('', 301, ['Location' => 'https://example.com/']))->assertResponseRedirects('https://example.com/'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('is redirected and has header "Location" with value "https://example.com/".'); + $this->getResponseTester(new Response('', 301))->assertResponseRedirects('https://example.com/'); + } + + public function testAssertResponseRedirectsWithStatusCode() + { + $this->getResponseTester(new Response('', 302))->assertResponseRedirects(null, 302); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('is redirected and status code is 301.'); + $this->getResponseTester(new Response('', 302))->assertResponseRedirects(null, 301); + } + + public function testAssertResponseRedirectsWithLocationAndStatusCode() + { + $this->getResponseTester(new Response('', 302, ['Location' => 'https://example.com/']))->assertResponseRedirects('https://example.com/', 302); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('is redirected and has header "Location" with value "https://example.com/" and status code is 301.'); + $this->getResponseTester(new Response('', 302))->assertResponseRedirects('https://example.com/', 301); + } + + public function testAssertResponseHasHeader() + { + $this->getResponseTester(new Response())->assertResponseHasHeader('Date'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Response has header "X-Date".'); + $this->getResponseTester(new Response())->assertResponseHasHeader('X-Date'); + } + + public function testAssertResponseNotHasHeader() + { + $this->getResponseTester(new Response())->assertResponseNotHasHeader('X-Date'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Response does not have header "Date".'); + $this->getResponseTester(new Response())->assertResponseNotHasHeader('Date'); + } + + public function testAssertResponseHeaderSame() + { + $this->getResponseTester(new Response())->assertResponseHeaderSame('Cache-Control', 'no-cache, private'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Response has header "Cache-Control" with value "public".'); + $this->getResponseTester(new Response())->assertResponseHeaderSame('Cache-Control', 'public'); + } + + public function testAssertResponseHeaderNotSame() + { + $this->getResponseTester(new Response())->assertResponseHeaderNotSame('Cache-Control', 'public'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Response does not have header "Cache-Control" with value "no-cache, private".'); + $this->getResponseTester(new Response())->assertResponseHeaderNotSame('Cache-Control', 'no-cache, private'); + } + + public function testAssertResponseHasCookie() + { + $response = new Response(); + $response->headers->setCookie(HttpFoundationCookie::create('foo', 'bar')); + + $this->getResponseTester($response)->assertResponseHasCookie('foo'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Response has cookie "bar".'); + $this->getResponseTester($response)->assertResponseHasCookie('bar'); + } + + public function testAssertResponseNotHasCookie() + { + $response = new Response(); + $response->headers->setCookie(HttpFoundationCookie::create('foo', 'bar')); + + $this->getResponseTester($response)->assertResponseNotHasCookie('bar'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Response does not have cookie "foo".'); + $this->getResponseTester($response)->assertResponseNotHasCookie('foo'); + } + + public function testAssertResponseCookieValueSame() + { + $response = new Response(); + $response->headers->setCookie(HttpFoundationCookie::create('foo', 'bar')); + + $this->getResponseTester($response)->assertResponseCookieValueSame('foo', 'bar'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('has cookie "bar" and has cookie "bar" with value "bar".'); + $this->getResponseTester($response)->assertResponseCookieValueSame('bar', 'bar'); + } + + public function testAssertBrowserHasCookie() + { + $this->getClientTester()->assertBrowserHasCookie('foo', '/path'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Browser has cookie "bar".'); + $this->getClientTester()->assertBrowserHasCookie('bar'); + } + + public function testAssertBrowserNotHasCookie() + { + $this->getClientTester()->assertBrowserNotHasCookie('bar'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Browser does not have cookie "foo" with path "/path".'); + $this->getClientTester()->assertBrowserNotHasCookie('foo', '/path'); + } + + public function testAssertBrowserCookieValueSame() + { + $this->getClientTester()->assertBrowserCookieValueSame('foo', 'bar', false, '/path'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('has cookie "foo" with path "/path" and has cookie "foo" with path "/path" with value "babar".'); + $this->getClientTester()->assertBrowserCookieValueSame('foo', 'babar', false, '/path'); + } + + public function testAssertSelectorExists() + { + $this->getCrawlerTester(new Crawler('