Skip to content
Edit this page

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.

Symfony integrates with an independent library called PHPUnit to give you a rich testing framework. This article covers the PHPUnit basics you'll need to write Symfony tests. To learn everything about PHPUnit and its features, read the official PHPUnit documentation.

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 (also known as functional 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.

Before creating your first test, install symfony/test-pack, which installs some other packages needed for testing (such as phpunit/phpunit):

1
$ composer require --dev symfony/test-pack

After the library is installed, try running PHPUnit:

1
$ 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").

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:

1
2
3
4
5
6
7
8
# 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

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 KernelTestCase class to help you creating and booting the kernel in your tests using bootKernel():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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):

1
2
# .env.test
KERNEL_CLASS=App\Kernel

The tests create a kernel that runs in the test 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:

1
2
3
# config/packages/test/twig.yaml
twig:
    strict_variables: 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:

1
2
3
4
self::bootKernel([
    'environment' => 'my_test_env',
    'debug'       => false,
]);

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:

1
2
3
4
# .env.test

# ...
DATABASE_URL="mysql://db_user:[email protected]: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):

  1. .env: containing env vars with application defaults;
  2. .env.test: overriding/setting specific test values or vars;
  3. .env.test.local: overriding settings specific for this machine.

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():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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 private services.

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ...
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.

Tests that interact with the database should use their own separate database to not mess with the databases used in the other 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:

1
2
# .env.test.local
DATABASE_URL="mysql://USERNAME:[email protected]: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 using multiple .env files in Symfony applications.

After that, you can create the test database and all tables using:

1
2
3
4
5
# 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

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:

1
$ composer require --dev dama/doctrine-test-bundle

Now, enable it as a PHPUnit extension:

1
2
3
4
5
6
7
8
<!-- phpunit.xml.dist -->
<phpunit>
    <!-- ... -->

    <extensions>
        <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
    </extensions>
</phpunit>

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.

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:

1
$ composer require --dev doctrine/doctrine-fixtures-bundle

Then, use the make:fixtures command of the SymfonyMakerBundle to generate an empty fixture class:

1
2
3
4
$ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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:

1
$ php bin/console --env=test doctrine:fixtures:load

For more information, read the DoctrineFixturesBundle documentation.

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:

  1. Make a request;
  2. Interact with the page (e.g. click on a link or submit a form);
  3. Test the response;
  4. Rinse and repeat.

Application tests are PHP files that typically live in the tests/Controller/ directory of your application. They often extend WebTestCase. This class adds special logic on top of the KernelTestCase. You can read more about that in the above 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:

1
2
3
4
5
6
7
$ 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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 <h1> 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):

1
2
$crawler = $client->request('GET', '/post/hello-world');
$this->assertCount(4, $crawler->filter('.comment'));

You can learn more about the crawler in The DOM Crawler.

The test client simulates an HTTP client like a browser and makes requests into your Symfony application:

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

The request() method takes the HTTP method and a URL as arguments and returns a Crawler instance.

The full signature of the request() method is:

1
2
3
4
5
6
7
8
9
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:

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 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 compiler pass to remove the kernel.reset tag from some services in your test environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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');

            // ...
        }
    }
}

The Client supports many operations that can be done in a real browser:

1
2
3
4
5
6
$client->back();
$client->forward();
$client->reload();

// clears all cookies and the history
$client->restart();

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:

1
$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:

1
$client->followRedirects();

If you pass false to the followRedirects() method, the redirects will no longer be followed:

1
$client->followRedirects(false);

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 $client->loginUser() to simulate a login request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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('[email protected]');

        // 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 UserInterface instance to loginUser(). This method creates a special 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 loginUser() method.

To set a specific firewall (main is set by default):

1
$client->loginUser($testUser, 'my_firewall');

The client provides an xmlHttpRequest() method, which has the same arguments as the request() method and is a shortcut to make AJAX requests:

1
2
// the required HTTP_X_REQUESTED_WITH header is added automatically
$client->xmlHttpRequest('POST', '/submit', ['name' => 'Fabien']);

If your application behaves according to some HTTP headers, pass them as the second argument of createClient():

1
2
3
4
$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:

1
2
3
4
$client->request('GET', '/', [], [], [
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
]);

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:

1
$client->catchExceptions(false);

If you use the client to test your application, you might want to access the client's internal objects:

1
2
$history = $client->getHistory();
$cookieJar = $client->getCookieJar();

You can also get the objects related to the latest request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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();

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:

1
2
3
4
5
6
7
// 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 How to Use the Profiler in a Functional Test article.

Like a real browser, the Client and Crawler objects can be used to interact with the page you're served:

Use the clickLink() method to click on the first link that contains the given text (or the first clickable image with that alt attribute):

1
2
3
4
$client = static::createClient();
$client->request('GET', '/post/hello-world');

$client->clickLink('Click here');

If you need access to the Link object that provides helpful methods specific to links (such as getMethod() and getUri()), use the Crawler::selectLink() method instead:

1
2
3
4
5
6
7
8
$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);

Use the submitForm() method to submit the form that contains the given button:

1
2
3
4
5
6
$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 or name of any <button> or <input type="submit"> included in the form. The second optional argument is used to override the default form field values.

If you need access to the Form object that provides helpful methods specific to forms (such as getUri(), getValues() and getFiles()) use the Crawler::selectButton() method instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$client = static::createClient();
$crawler = $client->request('GET', '/post/hello-world');

// select the button
$buttonCrawlerNode = $crawler->selectButton('submit');

// retrieve the Form object for the form belonging to this button
$form = $buttonCrawlerNode->form();

// set values on a form object
$form['my_form[name]'] = 'Fabien';
$form['my_form[subject]'] = 'Symfony rocks!';

// submit the Form object
$client->submit($form);

// optionally, you can combine the last 2 steps by passing an array of
// field values while submitting the form:
$client->submit($form, [
    'my_form[name]'    => 'Fabien',
    'my_form[subject]' => 'Symfony rocks!',
]);

Based on the form type, you can use different methods to fill in the input:

1
2
3
4
5
6
7
8
9
10
11
12
// selects an option or a radio
$form['my_form[country]']->select('France');

// ticks a checkbox
$form['my_form[like_symfony]']->tick();

// uploads a file
$form['my_form[photo]']->upload('/path/to/lucas.jpg');

// In the case of a multiple file upload
$form['my_form[field][0]']->upload('/path/to/lucas.jpg');
$form['my_form[field][1]']->upload('/path/to/lisa.jpg');

Now that the tests have visited a page and interacted with it (e.g. filled in a form), it is time to verify that the expected output is shown.

As all tests are based on PHPUnit, you can use any PHPUnit Assertion in your tests. Combined with test Client and the Crawler, this allows you to check anything you want.

However, Symfony provides useful shortcut methods for the most common cases:

assertResponseIsSuccessful(string $message = '', bool $verbose = true)
Asserts that the response was successful (HTTP status is 2xx).
assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true)
Asserts a specific HTTP status code.
assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', bool $verbose = true)
Asserts the response is a redirect response (optionally, you can check the target location and status code). The excepted location can be either an absolute or a relative path.
assertResponseHasHeader(string $headerName, string $message = '')/assertResponseNotHasHeader(string $headerName, string $message = '')
Asserts the given header is (not) available on the response, e.g. assertResponseHasHeader('content-type');.
assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = '')/assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = '')
Asserts the given header does (not) contain the expected value on the response, e.g. assertResponseHeaderSame('content-type', 'application/octet-stream');.
assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')/assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')
Asserts the given cookie is present in the response (optionally checking for a specific cookie path or domain).
assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = '')
Asserts the given cookie is present and set to the expected value.
assertResponseFormatSame(?string $expectedFormat, string $message = '')
Asserts the response format returned by the getFormat() method is the same as the expected value.
assertResponseIsUnprocessable(string $message = '', bool $verbose = true)
Asserts the response is unprocessable (HTTP status is 422)

assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = '')
Asserts the given request attribute is set to the expected value.
assertRouteSame($expectedRoute, array $parameters = [], string $message = '')
Asserts the request matches the given route and optionally route parameters.

assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')/assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')
Asserts that the test Client does (not) have the given cookie set (meaning, the cookie was set by any response in the test).
assertBrowserCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = '')
Asserts the given cookie in the test Client is set to the expected value.
assertThatForClient(Constraint $constraint, string $message = '')

Asserts the given Constraint in the Client. Useful for using your custom asserts in the same way as built-in asserts (i.e. without passing the Client as argument):

1
2
3
4
5
// add this method in some custom class imported in your tests
protected static function assertMyOwnCustomAssert(): void
{
    self::assertThatForClient(new SomeCustomConstraint());
}

assertSelectorExists(string $selector, string $message = '')/assertSelectorNotExists(string $selector, string $message = '')
Asserts that the given selector does (not) match at least one element in the response.
assertSelectorCount(int $expectedCount, string $selector, string $message = '')
Asserts that the expected number of selector elements are in the response
assertSelectorTextContains(string $selector, string $text, string $message = '')/assertSelectorTextNotContains(string $selector, string $text, string $message = '')
Asserts that the first element matching the given selector does (not) contain the expected text.
assertAnySelectorTextContains(string $selector, string $text, string $message = '')/assertAnySelectorTextNotContains(string $selector, string $text, string $message = '')
Asserts that any element matching the given selector does (not) contain the expected text.
assertSelectorTextSame(string $selector, string $text, string $message = '')
Asserts that the contents of the first element matching the given selector does equal the expected text.
assertAnySelectorTextSame(string $selector, string $text, string $message = '')
Asserts that the any element matching the given selector does equal the expected text.
assertPageTitleSame(string $expectedTitle, string $message = '')
Asserts that the <title> element is equal to the given title.
assertPageTitleContains(string $expectedTitle, string $message = '')
Asserts that the <title> element contains the given title.
assertInputValueSame(string $fieldName, string $expectedValue, string $message = '')/assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = '')
Asserts that value of the form input with the given name does (not) equal the expected value.
assertCheckboxChecked(string $fieldName, string $message = '')/assertCheckboxNotChecked(string $fieldName, string $message = '')
Asserts that the checkbox with the given name is (not) checked.
assertFormValue(string $formSelector, string $fieldName, string $value, string $message = '')/assertNoFormValue(string $formSelector, string $fieldName, string $message = '')
Asserts that value of the field of the first form matching the given selector does (not) equal the expected value.

assertEmailCount(int $count, ?string $transport = null, string $message = '')
Asserts that the expected number of emails was sent.
assertQueuedEmailCount(int $count, ?string $transport = null, string $message = '')
Asserts that the expected number of emails was queued (e.g. using the Messenger component).
assertEmailIsQueued(MessageEvent $event, string $message = '')/assertEmailIsNotQueued(MessageEvent $event, string $message = '')
Asserts that the given mailer event is (not) queued. Use getMailerEvent(int $index = 0, ?string $transport = null) to retrieve a mailer event by index.
assertEmailAttachmentCount(RawMessage $email, int $count, string $message = '')
Asserts that the given email has the expected number of attachments. Use getMailerMessage(int $index = 0, ?string $transport = null) to retrieve a specific email by index.
assertEmailTextBodyContains(RawMessage $email, string $text, string $message = '')/assertEmailTextBodyNotContains(RawMessage $email, string $text, string $message = '')
Asserts that the text body of the given email does (not) contain the expected text.
assertEmailHtmlBodyContains(RawMessage $email, string $text, string $message = '')/assertEmailHtmlBodyNotContains(RawMessage $email, string $text, string $message = '')
Asserts that the HTML body of the given email does (not) contain the expected text.
assertEmailHasHeader(RawMessage $email, string $headerName, string $message = '')/assertEmailNotHasHeader(RawMessage $email, string $headerName, string $message = '')
Asserts that the given email does (not) have the expected header set.
assertEmailHeaderSame(RawMessage $email, string $headerName, string $expectedValue, string $message = '')/assertEmailHeaderNotSame(RawMessage $email, string $headerName, string $expectedValue, string $message = '')
Asserts that the given email does (not) have the expected header set to the expected value.
assertEmailAddressContains(RawMessage $email, string $headerName, string $expectedValue, string $message = '')
Asserts that the given address header equals the expected e-mail address. This assertion normalizes addresses like Jane Smith <[email protected]> into [email protected].
assertEmailSubjectContains(RawMessage $email, string $expectedValue, string $message = '')/assertEmailSubjectNotContains(RawMessage $email, string $expectedValue, string $message = '')
Asserts that the subject of the given email does (not) contain the expected subject.

assertNotificationCount(int $count, ?string $transportName = null, string $message = '')
Asserts that the given number of notifications has been created (in total or for the given transport).
assertQueuedNotificationCount(int $count, ?string $transportName = null, string $message = '')
Asserts that the given number of notifications are queued (in total or for the given transport).
assertNotificationIsQueued(MessageEvent $event, string $message = '')
Asserts that the given notification is queued.
assertNotificationIsNotQueued(MessageEvent $event, string $message = '')
Asserts that the given notification is not queued.
assertNotificationSubjectContains(MessageInterface $notification, string $text, string $message = '')
Asserts that the given text is included in the subject of the given notification.
assertNotificationSubjectNotContains(MessageInterface $notification, string $text, string $message = '')
Asserts that the given text is not included in the subject of the given notification.
assertNotificationTransportIsEqual(MessageInterface $notification, string $transportName, string $message = '')
Asserts that the name of the transport for the given notification is the same as the given text.
assertNotificationTransportIsNotEqual(MessageInterface $notification, string $transportName, string $message = '')
Asserts that the name of the transport for the given notification is not the same as the given text.

assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array|null $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client')
Asserts that the given URL has been called using, if specified, the given method body and headers. By default it will check on the HttpClient, but you can also pass a specific HttpClient ID. (It will succeed if the request has been called multiple times.)
assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client')
Asserts that the given URL has not been called using GET or the specified method. By default it will check on the HttpClient, but a HttpClient id can be specified.
assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client')
Asserts that the given number of requests has been made on the HttpClient. By default it will check on the HttpClient, but you can also pass a specific HttpClient ID.

If you need to test the application as a whole, including the JavaScript code, you can use a real browser instead of the test client. This is called an end-to-end test and it's a great way to test the application.

This can be achieved thanks to the Panther component. You can learn more about it in the dedicated page.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version