Skip to content

Commit e02b521

Browse files
committed
[Testing] Reorganized "Application Tests" section
1 parent 161fb45 commit e02b521

File tree

3 files changed

+167
-126
lines changed

3 files changed

+167
-126
lines changed

.doctor-rst.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,4 @@ whitelist:
9595
- ".. _`Deploying Symfony 4 Apps on Heroku`: https://devcenter.heroku.com/articles/deploying-symfony4"
9696
- '.. versionadded:: 0.2' # MercureBundle
9797
- '.. code-block:: twig'
98+
- 'End to End Tests (E2E)'

testing.rst

+158-121
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ Symfony integrates with an independent library called `PHPUnit`_ to give
1515
you a rich testing framework. This article won't cover PHPUnit itself,
1616
which has its own excellent `documentation`_.
1717

18-
Before creating your first test, install PHPUnit with the following
19-
command:
18+
Before creating your first test, install ``phpunit/phpunit`` and the
19+
``symfony/test-pack``, which installs some other packages providing useful
20+
Symfony test utilities:
2021

2122
.. code-block:: terminal
2223
23-
$ composer require --dev phpunit/phpunit
24+
$ composer require --dev phpunit/phpunit symfony/test-pack
2425
2526
After the library is installed, try running PHPUnit:
2627

@@ -95,7 +96,9 @@ You can run tests using the ``./vendor/bin/phpunit`` command:
9596
.. tip::
9697

9798
In large test suites, it can make sense to create subdirectories for
98-
each type of tests (e.g. ``tests/unit/`` and ``test/functional/``).
99+
each type of tests (e.g. ``tests/Unit/`` and ``test/Functional/``).
100+
101+
.. _integration-tests:
99102

100103
Integration Tests
101104
-----------------
@@ -198,7 +201,15 @@ method::
198201
.. tip::
199202

200203
It is recommended to run your test with ``debug`` set to ``false`` on
201-
your CI server, as it significantly improves test performance.
204+
your CI server, as it significantly improves test performance. This
205+
disables clearing the cache. If your tests don't run in a clean
206+
environment each time, you have to manually clear it using for instance
207+
this code in ``tests/bootstrap.php``::
208+
209+
// ...
210+
211+
// ensure a fresh cache when debug mode is disabled
212+
(new \Symfony\Component\Filesystem\Filesystem())->remove(__DIR__.'/../var/cache/test');
202213

203214
Customizing Environment Variables
204215
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -243,7 +254,7 @@ the container is stored in ``self::$container``::
243254
{
244255
public function testSomething()
245256
{
246-
// boot the Symfony kernel
257+
// (1) boot the Symfony kernel
247258
self::bootKernel();
248259

249260
// (2) use self::$container to access the service container
@@ -267,6 +278,8 @@ It gives you access to both the public services and the non-removed
267278
are not used by any other services), you need to declare those private
268279
services as public in the ``config/services_test.yaml`` file.
269280

281+
.. _testing-databases:
282+
270283
Configuring a Database for Tests
271284
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
272285

@@ -410,21 +423,35 @@ have a very specific workflow:
410423
#. Test the response;
411424
#. Rinse and repeat.
412425

413-
Before creating your first test, install the ``symfony/test-pack`` which
414-
requires multiple packages providing some of the utilities used in the
415-
tests:
416-
417-
.. code-block:: terminal
426+
.. note::
418427

419-
$ composer require --dev symfony/test-pack
428+
The tools used in this section can be installed via the ``symfony/test-pack``,
429+
use ``composer require symfony/test-pack`` if you haven't done so already.
420430

421431
Write Your First Application Test
422432
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
423433

424434
Application tests are PHP files that typically live in the ``tests/Controller``
425-
directory of your application. If you want to test the pages handled by your
426-
``PostController`` class, start by creating a new ``PostControllerTest.php``
427-
file that extends a special ``WebTestCase`` class::
435+
directory of your application. They often extend
436+
:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase`. This class
437+
adds special logic on top of the ``KernelTestCase``. You can read more
438+
about that in the above :ref:`section on integration tests <integration-tests>`.
439+
440+
If you want to test the pages handled by your
441+
``PostController`` class, start by creating a new ``PostControllerTest``
442+
using the ``make:test`` command of the `SymfonyMakerBundle`_:
443+
444+
.. code-block:: terminal
445+
446+
$ php bin/console make:test
447+
448+
Which test type would you like?:
449+
> WebTestCase
450+
451+
The name of the test class (e.g. BlogPostTest):
452+
> Controller\PostControllerTest
453+
454+
This creates the following test class::
428455

429456
// tests/Controller/PostControllerTest.php
430457
namespace App\Tests\Controller;
@@ -433,87 +460,36 @@ file that extends a special ``WebTestCase`` class::
433460

434461
class PostControllerTest extends WebTestCase
435462
{
436-
public function testShowPost()
463+
public function testSomething(): void
437464
{
465+
// This calls KernelTestCase::bootKernel(), and creates a
466+
// "client" that is acting as the browser
438467
$client = static::createClient();
439468

440-
$client->request('GET', '/post/hello-world');
469+
// Request a specific page
470+
$crawler = $client->request('GET', '/');
441471

442-
$this->assertEquals(200, $client->getResponse()->getStatusCode());
472+
// Validate a successful response and some content
473+
$this->assertResponseIsSuccessful();
474+
$this->assertSelectorTextContains('h1', 'Hello World');
443475
}
444476
}
445477

446-
In the above example, you validated that the HTTP response was successful. The
447-
next step is to validate that the page actually contains the expected content.
448-
The ``createClient()`` method returns a client, which is like a browser that
449-
you'll use to crawl your site::
478+
In the above example, the test validates that the HTTP response was successful
479+
and the request body contains a ``<h1>`` tag with ``"Hello world"``. The
480+
``createClient()`` method also returns a client, which is like a browser
481+
that you can use to crawl your site::
450482

451483
$crawler = $client->request('GET', '/post/hello-world');
452484

485+
// for instance, count the number of ``.comment`` elements on the page
486+
$this->assertCount(4, $crawler->filter('.comment'));
487+
453488
The ``request()`` method (read
454489
:ref:`more about the request method <testing-request-method-sidebar>`)
455490
returns a :class:`Symfony\\Component\\DomCrawler\\Crawler` object which can
456491
be used to select elements in the response, click on links and submit forms.
457492

458-
Useful Assertions
459-
~~~~~~~~~~~~~~~~~
460-
461-
To get you started faster, here is a list of the most common and
462-
useful test assertions::
463-
464-
use Symfony\Component\HttpFoundation\Response;
465-
466-
// ...
467-
468-
// asserts that there is at least one h2 tag with the class "subtitle"
469-
// the third argument is an optional message shown on failed tests
470-
$this->assertGreaterThan(0, $crawler->filter('h2.subtitle')->count(),
471-
'There is at least one subtitle'
472-
);
473-
474-
// asserts that there are exactly 4 h2 tags on the page
475-
$this->assertCount(4, $crawler->filter('h2'));
476-
477-
// asserts that the "Content-Type" header is "application/json"
478-
$this->assertResponseHeaderSame('Content-Type', 'application/json');
479-
// equivalent to:
480-
$this->assertTrue($client->getResponse()->headers->contains(
481-
'Content-Type', 'application/json'
482-
));
483-
484-
// asserts that the response content contains a string
485-
$this->assertContains('foo', $client->getResponse()->getContent());
486-
// ...or matches a regex
487-
$this->assertRegExp('/foo(bar)?/', $client->getResponse()->getContent());
488-
489-
// asserts that the response status code is 2xx
490-
$this->assertResponseIsSuccessful();
491-
// equivalent to:
492-
$this->assertTrue($client->getResponse()->isSuccessful());
493-
494-
// asserts that the response status code is 404 Not Found
495-
$this->assertTrue($client->getResponse()->isNotFound());
496-
497-
// asserts a specific status code
498-
$this->assertResponseStatusCodeSame(201);
499-
// HTTP status numbers are available as constants too:
500-
// e.g. 201 === Symfony\Component\HttpFoundation\Response::HTTP_CREATED
501-
// equivalent to:
502-
$this->assertEquals(201, $client->getResponse()->getStatusCode());
503-
504-
// asserts that the response is a redirect to /demo/contact
505-
$this->assertResponseRedirects('/demo/contact');
506-
// equivalent to:
507-
$this->assertTrue($client->getResponse()->isRedirect('/demo/contact'));
508-
// ...or check that the response is a redirect to any URL
509-
$this->assertResponseRedirects();
510-
511-
.. versionadded:: 4.3
512-
513-
The ``assertResponseHeaderSame()``, ``assertResponseIsSuccessful()``,
514-
``assertResponseStatusCodeSame()``, ``assertResponseRedirects()`` and other
515-
related methods were introduced in Symfony 4.3.
516-
517493
Working with the Test Client
518494
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
519495

@@ -527,41 +503,9 @@ returns a ``Crawler`` instance.
527503

528504
.. tip::
529505

530-
Hardcoding the request URLs is a best practice for application tests. If the
531-
test generates URLs using the Symfony router, it won't detect any change
532-
made to the application URLs which may impact the end users.
533-
534-
.. _testing-request-method-sidebar:
535-
536-
.. sidebar:: More about the ``request()`` Method:
537-
538-
The full signature of the ``request()`` method is::
539-
540-
request(
541-
$method,
542-
$uri,
543-
array $parameters = [],
544-
array $files = [],
545-
array $server = [],
546-
$content = null,
547-
$changeHistory = true
548-
)
549-
550-
The ``server`` array is the raw values that you'd expect to normally
551-
find in the PHP `$_SERVER`_ superglobal. For example, to set the
552-
``Content-Type`` and ``Referer`` HTTP headers, you'd pass the following (mind
553-
the ``HTTP_`` prefix for non standard headers)::
554-
555-
$client->request(
556-
'GET',
557-
'/post/hello-world',
558-
[],
559-
[],
560-
[
561-
'CONTENT_TYPE' => 'application/json',
562-
'HTTP_REFERER' => '/foo/bar',
563-
]
564-
);
506+
Hardcoding the request URLs is a best practice for application tests.
507+
If the test generates URLs using the Symfony router, it won't detect
508+
any change made to the application URLs which may impact the end users.
565509

566510
Use the crawler to find DOM elements in the response. These elements can then
567511
be used to click on links and submit forms::
@@ -622,6 +566,38 @@ script::
622566

623567
$client->insulate();
624568

569+
.. _testing-request-method-sidebar:
570+
571+
.. sidebar:: More about the ``request()`` Method:
572+
573+
The full signature of the ``request()`` method is::
574+
575+
request(
576+
$method,
577+
$uri,
578+
array $parameters = [],
579+
array $files = [],
580+
array $server = [],
581+
$content = null,
582+
$changeHistory = true
583+
)
584+
585+
The ``server`` array is the raw values that you'd expect to normally
586+
find in the PHP `$_SERVER`_ superglobal. For example, to set the
587+
``Content-Type`` and ``Referer`` HTTP headers, you'd pass the following (mind
588+
the ``HTTP_`` prefix for non standard headers)::
589+
590+
$client->request(
591+
'GET',
592+
'/post/hello-world',
593+
[],
594+
[],
595+
[
596+
'CONTENT_TYPE' => 'application/json',
597+
'HTTP_REFERER' => '/foo/bar',
598+
]
599+
);
600+
625601
AJAX Requests
626602
.............
627603

@@ -757,6 +733,65 @@ to be reported by PHPUnit::
757733

758734
$client->catchExceptions(false);
759735

736+
Useful Assertions
737+
~~~~~~~~~~~~~~~~~
738+
739+
To get you started faster, here is a list of the most common and
740+
useful test assertions::
741+
742+
use Symfony\Component\HttpFoundation\Response;
743+
744+
// ...
745+
746+
// asserts that there is at least one h2 tag with the class "subtitle"
747+
// the third argument is an optional message shown on failed tests
748+
$this->assertGreaterThan(0, $crawler->filter('h2.subtitle')->count(),
749+
'There is at least one subtitle'
750+
);
751+
752+
// asserts that there are exactly 4 h2 tags on the page
753+
$this->assertCount(4, $crawler->filter('h2'));
754+
755+
// asserts that the "Content-Type" header is "application/json"
756+
$this->assertResponseHeaderSame('Content-Type', 'application/json');
757+
// equivalent to:
758+
$this->assertTrue($client->getResponse()->headers->contains(
759+
'Content-Type', 'application/json'
760+
));
761+
762+
// asserts that the response content contains a string
763+
$this->assertContains('foo', $client->getResponse()->getContent());
764+
// ...or matches a regex
765+
$this->assertRegExp('/foo(bar)?/', $client->getResponse()->getContent());
766+
767+
// asserts that the response status code is 2xx
768+
$this->assertResponseIsSuccessful();
769+
// equivalent to:
770+
$this->assertTrue($client->getResponse()->isSuccessful());
771+
772+
// asserts that the response status code is 404 Not Found
773+
$this->assertTrue($client->getResponse()->isNotFound());
774+
775+
// asserts a specific status code
776+
$this->assertResponseStatusCodeSame(201);
777+
// HTTP status numbers are available as constants too:
778+
// e.g. 201 === Symfony\Component\HttpFoundation\Response::HTTP_CREATED
779+
// equivalent to:
780+
$this->assertEquals(201, $client->getResponse()->getStatusCode());
781+
782+
// asserts that the response is a redirect to /demo/contact
783+
$this->assertResponseRedirects('/demo/contact');
784+
// equivalent to:
785+
$this->assertTrue($client->getResponse()->isRedirect('/demo/contact'));
786+
// ...or check that the response is a redirect to any URL
787+
$this->assertResponseRedirects();
788+
789+
.. versionadded:: 4.3
790+
791+
The ``assertResponseHeaderSame()``, ``assertResponseIsSuccessful()``,
792+
``assertResponseStatusCodeSame()``, ``assertResponseRedirects()`` and other
793+
related methods were introduced in Symfony 4.3.
794+
760795
.. index::
761796
single: Tests; Crawler
762797

@@ -988,10 +1023,10 @@ their type::
9881023
$client->submit($form, [], ['HTTP_ACCEPT_LANGUAGE' => 'es']);
9891024
$client->submitForm($button, [], 'POST', ['HTTP_ACCEPT_LANGUAGE' => 'es']);
9901025

991-
End to End Tests (E2E)
992-
----------------------
993-
9941026
.. TODO
1027+
End to End Tests (E2E)
1028+
----------------------
1029+
9951030
* panther
9961031
* testing javascript
9971032
* UX or form collections as example?
@@ -1042,8 +1077,8 @@ configuration adds tests from a custom ``lib/tests`` directory:
10421077
<!-- ... -->
10431078
</phpunit>
10441079
1045-
To include other directories in the code coverage, also edit the ``<filter>``
1046-
section:
1080+
To include other directories in the `code coverage analysis`, also edit the
1081+
``<filter>`` section:
10471082

10481083
.. code-block:: xml
10491084
@@ -1078,6 +1113,8 @@ Learn more
10781113
.. _`documentation`: https://phpunit.readthedocs.io/
10791114
.. _`Writing Tests for PHPUnit`: https://phpunit.readthedocs.io/en/stable/writing-tests-for-phpunit.html
10801115
.. _`unit test`: https://en.wikipedia.org/wiki/Unit_testing
1116+
.. _`DAMADoctrineTestBundle`: https://github.com/dmaicher/doctrine-test-bundle
1117+
.. _`DoctrineFixturesBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
10811118
.. _`$_SERVER`: https://www.php.net/manual/en/reserved.variables.server.php
10821119
.. _`SymfonyMakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html
10831120
.. _`code coverage analysis`: https://phpunit.readthedocs.io/en/9.1/code-coverage-analysis.html

0 commit comments

Comments
 (0)