Symfony Functional Tests
Requires Production

Symfony provides a useful framework for functional tests, working with PHPUnit.

If you already implemented such tests, you may want to re-use them for your performance tests using Blackfire Builds, and avoid to write your test scenarios twice.

Using the Blackfire SDK, you can leverage these functional tests in order to create Blackfire Builds, directly from your CI system.

It uses Symfony Panther in the background, in order to send real HTTP requests, and to manage the PHP built-in webserver.

Despite the use of Symfony Panther, this integration does not use ChromeDriver nor GeckoDriver, and thus does not support JavaScript.

  • PHP >= 7.1
  • Symfony >= 4.x
  • PHPUnit >= 8.x

Add Blackfire PHP SDK as a dependency in your project, together with Symfony Panther:

1
Loading...

Edit your phpunit.xml.dist:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- phpunit.xml.dist -->
    <extensions>
        <!-- Add Symfony Panther extension -->
        <extension class="Symfony\Component\Panther\ServerExtension" />
        <!-- Blackfire extension -->
        <extension class="\Blackfire\Bridge\PhpUnit\BlackfireBuildExtension">
            <arguments>
                <string><!-- Your environment name or UUID --></string>
                <string><!-- Name for the build (optional) --></string>
            </arguments>
        </extension>
    </extensions>

In order to use your functional tests with Blackfire, you need to either extend the BlackfireTestCase or import the BlackfireTestCaseTrait.

Then, you can use the BlackfiredHttpBrowserClient instead of the Symfony functional test client.

Extending BlackfireTestCase:

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
namespace App\Tests\Controller;

use App\Pagination\Paginator;
use Blackfire\Bridge\PhpUnit\BlackfireTestCase;

class BlogControllerTest extends BlackfireTestCase
{
    // Give a title to the scenario.
    protected const BLACKFIRE_SCENARIO_TITLE = 'Blog Controller';

    public function testIndex(): void
    {
        // Create the Blackfire enabled HTTP browser.
        $client = static::createBlackfiredHttpBrowserClient();
        $crawler = $client->request('GET', '/en/blog/');

        $this->assertResponseIsSuccessful();

        $this->assertCount(
            Paginator::PAGE_SIZE,
            $crawler->filter('article.post'),
            'The homepage displays the right number of posts.'
        );
    }
}

Using BlackfireTestCaseTrait:

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
28
namespace App\Tests\Controller;

use App\Pagination\Paginator;
use Blackfire\Bridge\PhpUnit\BlackfireTestCaseTrait;
use PHPUnit\Framework\TestCase;

class BlogControllerTest extends TestCase
{
    use BlackfireTestCaseTrait;

    // Give a title to the scenario.
    protected const BLACKFIRE_SCENARIO_TITLE = 'Blog Controller';

    public function testIndex(): void
    {
        // Create the Blackfire enabled HTTP browser.
        $client = static::createBlackfiredHttpBrowserClient();
        $crawler = $client->request('GET', '/en/blog/');

        $this->assertResponseIsSuccessful();

        $this->assertCount(
            Paginator::PAGE_SIZE,
            $crawler->filter('article.post'),
            'The homepage displays the right number of posts.'
        );
    }
}

A scenario is created for each instance of BlackfireTestCase / BlackfireTestCaseTrait, each request being sent constituting a step of the scenario.

You can specify the title for the ongoing scenario by setting the BLACKFIRE_SCENARIO_TITLE class constant within your test case, like in the example above.

Behind the scenes, Panther automatically starts the PHP built-in server. Please refer to Panther documentation if you want to customize the webserver.

A nice alternative to the PHP built-in server is the Symfony Local Web Server.

To do this, you need to set the PANTHER_EXTERNAL_BASE_URI environment variable with the complete base URI of Symfony server endpoint:

1
2
3
4
<!-- phpunit.xml.dist -->
    <php>
        <server name="PANTHER_EXTERNAL_BASE_URI" value="https://localhost:8000" force="true" />
    </php>

You also need to ensure that it is running before launching the PHPUnit test suite.

By default, a scenario is created for each instance of BlackfireTestCase and BlackfireTestCaseTrait, each request being sent constituting a step of the scenario.

You may want to control this behavior and create the scenarios manually. To do this, you need to define a BLACKFIRE_SCENARIO_AUTO_START class constant in your test case, and set it to false.

You can then use the BuildHelper to create and end your scenarios:

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
28
29
30
31
32
namespace App\Tests\Controller;

use App\Pagination\Paginator;
use Blackfire\Bridge\PhpUnit\BlackfireTestCase;

class BlogControllerTest extends BlackfireTestCase
{
    // Disable Blackfire Scenario Auto-Start.
    protected const BLACKFIRE_SCENARIO_AUTO_START = false;

    public function testIndex(): void
    {
        // Create the scenario.
        $buildHelper = BuildHelper::getInstance();
        $buildHelper->createScenario('Title for my scenario');

        // Create the Blackfire enabled HTTP browser.
        $client = static::createBlackfiredHttpBrowserClient();
        $crawler = $client->request('GET', '/en/blog/');

        $this->assertResponseIsSuccessful();

        $this->assertCount(
            Paginator::PAGE_SIZE,
            $crawler->filter('article.post'),
            'The homepage displays the right number of posts.'
        );

        // Don't forget to end the scenario.
        $buildHelper->endCurrentScenario();
    }
}

By default, every requests sent by the BlackfiredHttpBrowserClient are profiled.

You may want to temporarily disable the profiling process for a few requests. This is possible using the disableProfiling() method:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
namespace App\Tests\Controller;

use App\Pagination\Paginator;
use Blackfire\Bridge\PhpUnit\BlackfireTestCase;

class BlogControllerTest extends BlackfireTestCase
{
    // Give a title to the scenario.
    protected const BLACKFIRE_SCENARIO_TITLE = 'Blog Controller';

    public function testNewComment(): void
    {
        // Create the Blackfire enabled HTTP browser.
        $client = static::createBlackfiredHttpBrowserClient();
        $client->followRedirects();

        // Find first blog post
        $crawler = $client->request('GET', '/en/blog/');
        $postLink = $crawler->filter('article.post > h2 a')->link();

        $client->click($postLink);
        $client->clickLink('Sign in');

        // Temporarily disable profiling.
        $client->disableProfiling();

        $client->submitForm('Sign in', [
            '_username' => 'john_user',
            '_password' => 'kitten',
        ]);

        // Re-enable profiling.
        $client->enableProfiling();

        $crawler = $client->submitForm('Publish comment', [
            'comment[content]' => 'Hi, Symfony!',
        ]);

        $newComment = $crawler->filter('.post-comment')->first()->filter('div > p')->text();

        $this->assertSame('Hi, Symfony!', $newComment);
    }
}

In order to compare the current build to another one, you may set BLACKFIRE_EXTERNAL_ID and BLACKFIRE_EXTERNAL_PARENT_ID environment variables when launching your tests:

1
2
3
BLACKFIRE_EXTERNAL_ID=current_build_reference \
BLACKFIRE_EXTERNAL_PARENT_ID=parent_build_reference \
bin/phpunit tests/Controller/BlogControllerTest

You may use Git commit identifiers as references.

You may want to run Blackfire tests in a separate job in your pipeline, while still running your functional tests.

In this case, it is possible to globally disable the Blackfire build by setting the BLACKFIRE_BUILD_DISABLED environment variable to 1:

1
BLACKFIRE_BUILD_DISABLED=1 bin/phpunit tests/Controller/BlogControllerTest

Doing so also disables profiling for every HTTP requests sent by the BlackfiredHttpBrowser.

The BlackfireTestCaseTrait leverages the setUpBeforeClass() and tearDownAfterClass() methods from the base PHPUnit TestCase class.

If you already use them within a test case that you want to use with Blackfire, the scenario auto-start will not work, as the mentioned methods will be overriden by the ones defined in your test case class.

In this case, you need to create the scenarios manually.