Testing ======= Whenever you write a new line of code, you also potentially add new bugs. To build better and more reliable applications, you should test your code using both functional and unit tests. .. _testing-installation: The PHPUnit Testing Framework ----------------------------- Symfony integrates with an independent library called `PHPUnit`_ to give you a rich testing framework. This article won't cover PHPUnit itself, which has its own excellent `documentation`_. Before creating your first test, install ``symfony/test-pack``, which installs some other packages needed for testing (such as ``phpunit/phpunit``): .. code-block:: terminal $ composer require --dev symfony/test-pack After the library is installed, try running PHPUnit: .. code-block:: terminal $ php bin/phpunit This command automatically runs your application tests. Each test is a PHP class ending with "Test" (e.g. ``BlogControllerTest``) that lives in the ``tests/`` directory of your application. PHPUnit is configured by the ``phpunit.xml.dist`` file in the root of your application. The default configuration provided by Symfony Flex will be enough in most cases. Read the `PHPUnit documentation`_ to discover all possible configuration options (e.g. to enable code coverage or to split your test into multiple "test suites"). .. note:: :ref:`Symfony Flex ` automatically creates ``phpunit.xml.dist`` and ``tests/bootstrap.php``. If these files are missing, you can try running the recipe again using ``composer recipes:install phpunit/phpunit --force -v``. Types of Tests -------------- There are many types of automated tests and precise definitions often differ from project to project. In Symfony, the following definitions are used. If you have learned something different, that is not necessarily wrong, just different from what the Symfony documentation is using. `Unit Tests`_ These tests ensure that *individual* units of source code (e.g. a single class) behave as intended. `Integration Tests`_ These tests test a combination of classes and commonly interact with Symfony's service container. These tests do not yet cover the fully working application, those are called *Application tests*. `Application Tests`_ Application tests test the behavior of a complete application. They make HTTP requests (both real and simulated ones) and test that the response is as expected. Unit Tests ---------- A `unit test`_ ensures that individual units of source code (e.g. a single class or some specific method in some class) meet their design and behave as intended. Writing unit tests in a Symfony application is no different from writing standard PHPUnit unit tests. You can learn about it in the PHPUnit documentation: `Writing Tests for PHPUnit`_. By convention, the ``tests/`` directory should replicate the directory of your application for unit tests. So, if you're testing a class in the ``src/Form/`` directory, put the test in the ``tests/Form/`` directory. Autoloading is automatically enabled via the ``vendor/autoload.php`` file (as configured by default in the ``phpunit.xml.dist`` file). You can run tests using the ``bin/phpunit`` command: .. code-block:: terminal # run all tests of the application $ php bin/phpunit # run all tests in the Form/ directory $ php bin/phpunit tests/Form # run tests for the UserType class $ php bin/phpunit tests/Form/UserTypeTest.php .. tip:: In large test suites, it can make sense to create subdirectories for each type of test (``tests/Unit/``, ``tests/Integration/``, ``tests/Application/``, etc.). .. _integration-tests: Integration Tests ----------------- An integration test will test a larger part of your application compared to a unit test (e.g. a combination of services). Integration tests might want to use the Symfony Kernel to fetch a service from the dependency injection container. Symfony provides a :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` class to help you creating and booting the kernel in your tests using ``bootKernel()``:: // tests/Service/NewsletterGeneratorTest.php namespace App\Tests\Service; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class NewsletterGeneratorTest extends KernelTestCase { public function testSomething(): void { self::bootKernel(); // ... } } The ``KernelTestCase`` also makes sure your kernel is rebooted for each test. This assures that each test is run independently from each other. To run your application tests, the ``KernelTestCase`` class needs to find the application kernel to initialize. The kernel class is usually defined in the ``KERNEL_CLASS`` environment variable (included in the default ``.env.test`` file provided by Symfony Flex): .. code-block:: env # .env.test KERNEL_CLASS=App\Kernel .. note:: If your use case is more complex, you can also override the ``getKernelClass()`` or ``createKernel()`` methods of your functional test, which takes precedence over the ``KERNEL_CLASS`` env var. Set-up your Test Environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The tests create a kernel that runs in the ``test`` :ref:`environment `. This allows to have special settings for your tests inside ``config/packages/test/``. If you have Symfony Flex installed, some packages already installed some useful test configuration. For example, by default, the Twig bundle is configured to be especially strict to catch errors before deploying your code to production: .. configuration-block:: .. code-block:: yaml # config/packages/test/twig.yaml twig: strict_variables: true .. code-block:: xml .. code-block:: php // config/packages/test/twig.php use Symfony\Config\TwigConfig; return static function (TwigConfig $twig): void { $twig->strictVariables(true); }; You can also use a different environment entirely, or override the default debug mode (``true``) by passing each as options to the ``bootKernel()`` method:: self::bootKernel([ 'environment' => 'my_test_env', 'debug' => false, ]); .. tip:: It is recommended to run your test with ``debug`` set to ``false`` on your CI server, as it significantly improves test performance. This disables clearing the cache. If your tests don't run in a clean environment each time, you have to manually clear it using for instance this code in ``tests/bootstrap.php``:: // ... // ensure a fresh cache when debug mode is disabled (new \Symfony\Component\Filesystem\Filesystem())->remove(__DIR__.'/../var/cache/test'); Customizing Environment Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you need to customize some environment variables for your tests (e.g. the ``DATABASE_URL`` used by Doctrine), you can do that by overriding anything you need in your ``.env.test`` file: .. code-block:: env # .env.test # ... DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name_test?serverVersion=8.0.37" In the test environment, these env files are read (if vars are duplicated in them, files lower in the list override previous items): #. ``.env``: containing env vars with application defaults; #. ``.env.test``: overriding/setting specific test values or vars; #. ``.env.test.local``: overriding settings specific for this machine. .. caution:: The ``.env.local`` file is **not** used in the test environment, to make each test set-up as consistent as possible. Retrieving Services in the Test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In your integration tests, you often need to fetch the service from the service container to call a specific method. After booting the kernel, the container is returned by ``static::getContainer()``:: // tests/Service/NewsletterGeneratorTest.php namespace App\Tests\Service; use App\Service\NewsletterGenerator; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class NewsletterGeneratorTest extends KernelTestCase { public function testSomething(): void { // (1) boot the Symfony kernel self::bootKernel(); // (2) use static::getContainer() to access the service container $container = static::getContainer(); // (3) run some service & test the result $newsletterGenerator = $container->get(NewsletterGenerator::class); $newsletter = $newsletterGenerator->generateMonthlyNews(/* ... */); $this->assertEquals('...', $newsletter->getContent()); } } The container from ``static::getContainer()`` is actually a special test container. It gives you access to both the public services and the non-removed :ref:`private services `. .. note:: If you need to test private services that have been removed (those who are not used by any other services), you need to declare those private services as public in the ``config/services_test.yaml`` file. Mocking Dependencies -------------------- Sometimes it can be useful to mock a dependency of a tested service. From the example in the previous section, let's assume the ``NewsletterGenerator`` has a dependency to a private alias ``NewsRepositoryInterface`` pointing to a private ``NewsRepository`` service and you'd like to use a mocked ``NewsRepositoryInterface`` instead of the concrete one:: // ... use App\Contracts\Repository\NewsRepositoryInterface; class NewsletterGeneratorTest extends KernelTestCase { public function testSomething(): void { // ... same bootstrap as the section above $newsRepository = $this->createMock(NewsRepositoryInterface::class); $newsRepository->expects(self::once()) ->method('findNewsFromLastMonth') ->willReturn([ new News('some news'), new News('some other news'), ]) ; $container->set(NewsRepositoryInterface::class, $newsRepository); // will be injected the mocked repository $newsletterGenerator = $container->get(NewsletterGenerator::class); // ... } } No further configuration is required, as the test service container is a special one that allows you to interact with private services and aliases. .. _testing-databases: Configuring a Database for Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests that interact with the database should use their own separate database to not mess with the databases used in the other :ref:`configuration environments `. To do that, edit or create the ``.env.test.local`` file at the root directory of your project and define the new value for the ``DATABASE_URL`` env var: .. code-block:: env # .env.test.local DATABASE_URL="mysql://USERNAME:PASSWORD@127.0.0.1:3306/DB_NAME?serverVersion=8.0.37" This assumes that each developer/machine uses a different database for the tests. If the test set-up is the same on each machine, use the ``.env.test`` file instead and commit it to the shared repository. Learn more about :ref:`using multiple .env files in Symfony applications `. After that, you can create the test database and all tables using: .. code-block:: terminal # create the test database $ php bin/console --env=test doctrine:database:create # create the tables/columns in the test database $ php bin/console --env=test doctrine:schema:create .. tip:: You can run these commands to create the database during the :doc:`test bootstrap process `. .. tip:: A common practice is to append the ``_test`` suffix to the original database names in tests. If the database name in production is called ``project_acme`` the name of the testing database could be ``project_acme_test``. Resetting the Database Automatically Before each Test ..................................................... Tests should be independent from each other to avoid side effects. For example, if some test modifies the database (by adding or removing an entity) it could change the results of other tests. The `DAMADoctrineTestBundle`_ uses Doctrine transactions to let each test interact with an unmodified database. Install it using: .. code-block:: terminal $ composer require --dev dama/doctrine-test-bundle Now, enable it as a PHPUnit extension: .. code-block:: xml That's it! This bundle uses a clever trick: it begins a database transaction before every test and rolls it back automatically after the test finishes to undo all changes. Read more in the documentation of the `DAMADoctrineTestBundle`_. .. _doctrine-fixtures: Load Dummy Data Fixtures ........................ Instead of using the real data from the production database, it's common to use fake or dummy data in the test database. This is usually called *"fixtures data"* and Doctrine provides a library to create and load them. Install it with: .. code-block:: terminal $ composer require --dev doctrine/doctrine-fixtures-bundle Then, use the ``make:fixtures`` command of the `SymfonyMakerBundle`_ to generate an empty fixture class: .. code-block:: terminal $ php bin/console make:fixtures The class name of the fixtures to create (e.g. AppFixtures): > ProductFixture Then you modify and use this class to load new entities in the database. For instance, to load ``Product`` objects into Doctrine, use:: // src/DataFixtures/ProductFixture.php namespace App\DataFixtures; use App\Entity\Product; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; class ProductFixture extends Fixture { public function load(ObjectManager $manager): void { $product = new Product(); $product->setName('Priceless widget'); $product->setPrice(14.50); $product->setDescription('Ok, I guess it *does* have a price'); $manager->persist($product); // add more products $manager->flush(); } } Empty the database and reload *all* the fixture classes with: .. code-block:: terminal $ php bin/console --env=test doctrine:fixtures:load For more information, read the `DoctrineFixturesBundle documentation`_. .. _functional-tests: Application Tests ----------------- Application tests check the integration of all the different layers of the application (from the routing to the views). They are no different from unit tests or integration tests as far as PHPUnit is concerned, but they have a very specific workflow: #. :ref:`Make a request `; #. :ref:`Interact with the page ` (e.g. click on a link or submit a form); #. :ref:`Test the response `; #. Rinse and repeat. .. note:: The tools used in this section can be installed via the ``symfony/test-pack``, use ``composer require symfony/test-pack`` if you haven't done so already. Write Your First Application Test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Application tests are PHP files that typically live in the ``tests/Controller/`` directory of your application. They often extend :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase`. This class adds special logic on top of the ``KernelTestCase``. You can read more about that in the above :ref:`section on integration tests `. If you want to test the pages handled by your ``PostController`` class, start by creating a new ``PostControllerTest`` using the ``make:test`` command of the `SymfonyMakerBundle`_: .. code-block:: terminal $ php bin/console make:test Which test type would you like?: > WebTestCase The name of the test class (e.g. BlogPostTest): > Controller\PostControllerTest This creates the following test class:: // tests/Controller/PostControllerTest.php namespace App\Tests\Controller; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class PostControllerTest extends WebTestCase { public function testSomething(): void { // This calls KernelTestCase::bootKernel(), and creates a // "client" that is acting as the browser $client = static::createClient(); // Request a specific page $crawler = $client->request('GET', '/'); // Validate a successful response and some content $this->assertResponseIsSuccessful(); $this->assertSelectorTextContains('h1', 'Hello World'); } } In the above example, the test validates that the HTTP response was successful and the request body contains a ``

`` tag with ``"Hello world"``. The ``request()`` method also returns a crawler, which you can use to create more complex assertions in your tests (e.g. to count the number of page elements that match a given CSS selector):: $crawler = $client->request('GET', '/post/hello-world'); $this->assertCount(4, $crawler->filter('.comment')); You can learn more about the crawler in :doc:`/testing/dom_crawler`. .. _testing-applications-arrange: Making Requests ~~~~~~~~~~~~~~~ The test client simulates an HTTP client like a browser and makes requests into your Symfony application:: $crawler = $client->request('GET', '/post/hello-world'); The :method:`request() ` method takes the HTTP method and a URL as arguments and returns a ``Crawler`` instance. .. tip:: Hardcoding the request URLs is a best practice for application tests. If the test generates URLs using the Symfony router, it won't detect any change made to the application URLs which may impact the end users. The full signature of the ``request()`` method is:: public function request( string $method, string $uri, array $parameters = [], array $files = [], array $server = [], ?string $content = null, bool $changeHistory = true ): Crawler This allows you to create all types of requests you can think of: .. tip:: The test client is available as the ``test.client`` service in the container in the ``test`` environment (or wherever the :ref:`framework.test ` option is enabled). This means you can override the service entirely if you need to. Multiple Requests in One Test ............................. After making a request, subsequent requests will make the client reboot the kernel. This recreates the container from scratch to ensures that requests are isolated and use new service objects each time. This behavior can have some unexpected consequences: for example, the security token will be cleared, Doctrine entities will be detached, etc. First, you can call the client's :method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::disableReboot` method to reset the kernel instead of rebooting it. In practice, Symfony will call the ``reset()`` method of every service tagged with ``kernel.reset``. However, this will **also** clear the security token, detach Doctrine entities, etc. In order to solve this issue, create a :doc:`compiler pass ` to remove the ``kernel.reset`` tag from some services in your test environment:: // src/Kernel.php namespace App; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; class Kernel extends BaseKernel implements CompilerPassInterface { use MicroKernelTrait; // ... public function process(ContainerBuilder $container): void { if ('test' === $this->environment) { // prevents the security token to be cleared $container->getDefinition('security.token_storage')->clearTag('kernel.reset'); // prevents Doctrine entities to be detached $container->getDefinition('doctrine')->clearTag('kernel.reset'); // ... } } } Browsing the Site ................. The Client supports many operations that can be done in a real browser:: $client->back(); $client->forward(); $client->reload(); // clears all cookies and the history $client->restart(); .. note:: The ``back()`` and ``forward()`` methods skip the redirects that may have occurred when requesting a URL, as normal browsers do. Redirecting ........... When a request returns a redirect response, the client does not follow it automatically. You can examine the response and force a redirection afterwards with the ``followRedirect()`` method:: $crawler = $client->followRedirect(); If you want the client to automatically follow all redirects, you can force them by calling the ``followRedirects()`` method before performing the request:: $client->followRedirects(); If you pass ``false`` to the ``followRedirects()`` method, the redirects will no longer be followed:: $client->followRedirects(false); .. _testing_logging_in_users: Logging in Users (Authentication) ................................. When you want to add application tests for protected pages, you have to first "login" as a user. Reproducing the actual steps - such as submitting a login form - makes a test very slow. For this reason, Symfony provides a ``loginUser()`` method to simulate logging in your functional tests. Instead of logging in with real users, it's recommended to create a user only for tests. You can do that with `Doctrine data fixtures`_ to load the testing users only in the test database. After loading users in your database, use your user repository to fetch this user and use :method:`$client->loginUser() ` to simulate a login request:: // tests/Controller/ProfileControllerTest.php namespace App\Tests\Controller; use App\Repository\UserRepository; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class ProfileControllerTest extends WebTestCase { // ... public function testVisitingWhileLoggedIn(): void { $client = static::createClient(); $userRepository = static::getContainer()->get(UserRepository::class); // retrieve the test user $testUser = $userRepository->findOneByEmail('john.doe@example.com'); // simulate $testUser being logged in $client->loginUser($testUser); // test e.g. the profile page $client->request('GET', '/profile'); $this->assertResponseIsSuccessful(); $this->assertSelectorTextContains('h1', 'Hello John!'); } } You can pass any :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` instance to ``loginUser()``. This method creates a special :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\TestBrowserToken` object and stores in the session of the test client. If you need to define custom attributes in this token, you can use the ``tokenAttributes`` argument of the :method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::loginUser` method. To set a specific firewall (``main`` is set by default):: $client->loginUser($testUser, 'my_firewall'); .. note:: By design, the ``loginUser()`` method doesn't work when using stateless firewalls. Instead, add the appropriate token/header in each ``request()`` call. Making AJAX Requests .................... The client provides an :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::xmlHttpRequest` method, which has the same arguments as the ``request()`` method and is a shortcut to make AJAX requests:: // the required HTTP_X_REQUESTED_WITH header is added automatically $client->xmlHttpRequest('POST', '/submit', ['name' => 'Fabien']); Sending Custom Headers ...................... If your application behaves according to some HTTP headers, pass them as the second argument of ``createClient()``:: $client = static::createClient([], [ 'HTTP_HOST' => 'en.example.com', 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0', ]); You can also override HTTP headers on a per request basis:: $client->request('GET', '/', [], [], [ 'HTTP_HOST' => 'en.example.com', 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0', ]); .. caution:: The name of your custom headers must follow the syntax defined in the `section 4.1.18 of RFC 3875`_: replace ``-`` by ``_``, transform it into uppercase and prefix the result with ``HTTP_``. For example, if your header name is ``X-Session-Token``, pass ``HTTP_X_SESSION_TOKEN``. Reporting Exceptions .................... Debugging exceptions in application tests may be difficult because by default they are caught and you need to look at the logs to see which exception was thrown. Disabling catching of exceptions in the test client allows the exception to be reported by PHPUnit:: $client->catchExceptions(false); Accessing Internal Objects .......................... If you use the client to test your application, you might want to access the client's internal objects:: $history = $client->getHistory(); $cookieJar = $client->getCookieJar(); You can also get the objects related to the latest request:: // the HttpKernel request instance $request = $client->getRequest(); // the BrowserKit request instance $request = $client->getInternalRequest(); // the HttpKernel response instance $response = $client->getResponse(); // the BrowserKit response instance $response = $client->getInternalResponse(); // the Crawler instance $crawler = $client->getCrawler(); Accessing the Profiler Data ........................... On each request, you can enable the Symfony profiler to collect data about the internal handling of that request. For example, the profiler could be used to verify that a given page runs less than a certain number of database queries when loading. To get the profiler for the last request, do the following:: // enables the profiler for the very next request $client->enableProfiler(); $crawler = $client->request('GET', '/profiler'); // gets the profile $profile = $client->getProfile(); For specific details on using the profiler inside a test, see the :doc:`/testing/profiling` article. .. _testing-applications-act: Interacting with the Response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Like a real browser, the Client and Crawler objects can be used to interact with the page you're served: .. _testing-links: Clicking on Links ................. Use the ``clickLink()`` method to click on the first link that contains the given text (or the first clickable image with that ``alt`` attribute):: $client = static::createClient(); $client->request('GET', '/post/hello-world'); $client->clickLink('Click here'); If you need access to the :class:`Symfony\\Component\\DomCrawler\\Link` object that provides helpful methods specific to links (such as ``getMethod()`` and ``getUri()``), use the ``Crawler::selectLink()`` method instead:: $client = static::createClient(); $crawler = $client->request('GET', '/post/hello-world'); $link = $crawler->selectLink('Click here')->link(); // ... // use click() if you want to click the selected link $client->click($link); .. _testing-forms: Submitting Forms ................ Use the ``submitForm()`` method to submit the form that contains the given button:: $client = static::createClient(); $client->request('GET', '/post/hello-world'); $crawler = $client->submitForm('Add comment', [ 'comment_form[content]' => '...', ]); The first argument of ``submitForm()`` is the text content, ``id``, ``value`` or ``name`` of any ``