Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Loading Fixtures References

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Despite all our precautions, we still sometimes have enclosures with no security. Yea... a lot of people are getting eaten, a lot of lawsuits - very expensive. To help with this, I want to add an "Alarm" button on the homepage next to any enclosures that do not have active security.

Because this sounds pretty important, let's write the test first. Add public function testThatThereIsAnAlarmButtonWithoutSecurity():

... lines 1 - 8
class DefaultControllerTest extends WebTestCase
{
... lines 11 - 27
public function testThatThereIsAnAlarmButtonWithoutSecurity()
{
... lines 30 - 42
}
}

Copy the fixture and request code from before and paste it here:

... lines 1 - 8
class DefaultControllerTest extends WebTestCase
{
... lines 11 - 27
public function testThatThereIsAnAlarmButtonWithoutSecurity()
{
$fixtures = $this->loadFixtures([
LoadBasicParkData::class,
LoadSecurityData::class,
])->getReferenceRepository();
$client = $this->makeClient();
$crawler = $client->request('GET', '/');
... lines 38 - 42
}
}

But, at the end of loadFixtures(), add getReferenceRepository() and assign this to a new $fixtures variable:

... lines 1 - 8
class DefaultControllerTest extends WebTestCase
{
... lines 11 - 27
public function testThatThereIsAnAlarmButtonWithoutSecurity()
{
$fixtures = $this->loadFixtures([
LoadBasicParkData::class,
LoadSecurityData::class,
])->getReferenceRepository();
... lines 34 - 42
}
}

What are Fixture References?

Here's the deal: if you look in the fixtures, you can see that the first two Enclosures do not have any security. You can also see that we're using some sort of "reference" system. This allows us to store a specific object in memory so that we can re-use it somewhere else. For example, in LoadSecurityData, we get the herbivorous-enclosure object out and add security! We're safe from those wild veggie eating dinos!

It does the same for carnivorous-enclosure... but then adds two Security objects that are both inactive. Doh! Yep, this means that the carnivorous-enclosure is the only Enclosure that is not secure. In the test, our goal is to assert that, on the homepage, this exact Enclosure has the alarm button.

And we planned ahead for this! Remember, in the template, we added an enclosure-{id} to each tr element. So if we can get the actual id value of the Carnivorous Enclosure, it will be really easy to find its tr element and look for the alarm button. The reference system gives us that power!

Yep, we can fetch the exact Enclosure object by saying $enclosure = $fixtures->getReference('carnivorous-enclosure'):

... lines 1 - 8
class DefaultControllerTest extends WebTestCase
{
... lines 11 - 27
public function testThatThereIsAnAlarmButtonWithoutSecurity()
{
... lines 30 - 36
$crawler = $client->request('GET', '/');
$enclosure = $fixtures->getReference('carnivorous-enclosure');
... lines 40 - 42
}
}

Next, create a $selector variable set to sprintf('#enclosure-%s .button-alarm') and $enclosure->getId(). We'll expect the alarm button to have this class:

... lines 1 - 8
class DefaultControllerTest extends WebTestCase
{
... lines 11 - 27
public function testThatThereIsAnAlarmButtonWithoutSecurity()
{
... lines 30 - 38
$enclosure = $fixtures->getReference('carnivorous-enclosure');
$selector = sprintf('#enclosure-%s .button-alarm', $enclosure->getId());
... lines 41 - 42
}
}

Finish the test! $this->greaterThan(0, $crawler->filter($selector)->count()):

... lines 1 - 8
class DefaultControllerTest extends WebTestCase
{
... lines 11 - 27
public function testThatThereIsAnAlarmButtonWithoutSecurity()
{
... lines 30 - 41
$this->assertGreaterThan(0, $crawler->filter($selector)->count());
}
}

I love it! So first, of course, make sure the test fails. Copy the method name and run phpunit with the --filter option:

./vendor/bin/phpunit --filter testThatThereIsAnAlarmButtonWithoutSecurity

Awesome!

Adding the Alarm Button

So let's code! In index.html.twig, add one more <td>: if enclosure.isSecurityActive() with else and endif:

... lines 1 - 2
{% block body %}
... lines 4 - 5
<table class="table-enclosures">
<tbody>
{% for enclosure in enclosures %}
<tr id="enclosure-{{ enclosure.id }}">
... lines 10 - 13
<td>
{% if enclosure.isSecurityActive %}
... line 16
{% else %}
... line 18
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

If security is active, we rock! Add a cute lock icon and say "Security active". Yep, just sit back and enjoy some Jolt soda: nobody is getting eaten today!

... lines 1 - 2
{% block body %}
... lines 4 - 5
<table class="table-enclosures">
<tbody>
{% for enclosure in enclosures %}
<tr id="enclosure-{{ enclosure.id }}">
... lines 10 - 13
<td>
{% if enclosure.isSecurityActive %}
? Security active!
{% else %}
... line 18
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

But if security is not active, ah crap! Add the button with the button-alarm class that the test is looking for. And say "Sound alarm!!!":

... lines 1 - 2
{% block body %}
... lines 4 - 5
<table class="table-enclosures">
<tbody>
{% for enclosure in enclosures %}
<tr id="enclosure-{{ enclosure.id }}">
... lines 10 - 13
<td>
{% if enclosure.isSecurityActive %}
? Security active!
{% else %}
<button class="button button-alarm">Sound alarm !!!</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

That should be it! Run the test:

./vendor/bin/phpunit --filter testThatThereIsAnAlarmButtonWithoutSecurity

Ha! It passes!

Debugging Functional Tests

But... what if it didn't pass? Well... the errors wouldn't be very helpful: it would basically just say that 0 is not greater than 0. When things fail, the trick is to go above the failure and dump($client->getResponse()->getContent()). If you're using Flex, make sure to install the var-dumper package.

Tip

To install the symfony/var-dumper package, run: composer require --dev var-dumper

The --dev option tells Composer to install it as a dev dependency.

Now when you run the test, it will at least print out the HTML body. By the way, with a little bit of clever coding, you can hook into the onNotSuccessfulTest method and have the last response content printed automatically when a test fails. I'll leave that as a challenge for you. But, ask us in the comments if you have any questions.

Ok, there's one more thing I want to talk about with functional tests: filling out and submitting a form.

Leave a comment!

7
Login or Register to join the conversation
Angelika R. Avatar
Angelika R. Avatar Angelika R. | posted 1 year ago

I think there is only a part of index.html.twig in the script.

Reply

Hey Angelika,

Yeah, I see, code blocks are not perfect on this page unfortunately, we're sorry about that! I'm going to fix it, but for your information, our code blocks are "dynamic" (expandable), so you can expand any code block you need like you can do on GitHub - press arrows on the left of the line numbers. Or, to expand the whole code block - press the square icon in the left top corner of the code block :)

Cheers!

Reply
Angelika R. Avatar
Angelika R. Avatar Angelika R. | victor | posted 1 year ago

I know, it's a great feature, but on this page the template cannot be expanded fully. At least I cannot do it.

Reply

Hey Angelika,

Woops, I see now! That is definitely no go, I just fixed it.

Thank you for reporting, and sorry for any inconvenience!

Cheers!

Reply
Luc H. Avatar

I am trying to implement the last hint in this section, i.e. automatically writing the response when the test fails:

protected function onNotSuccessfulTest(Throwable $e): void
{
dump($e);
// dump($client->getResponse()->getContent())


parent::onNotSuccessfulTest($e);
}

When I look at the data from $e, I don't see a way to get to the client or response. Can you give me a hint on that?

Reply

Hey Luc H.

I think you need to hold the client and/or response instance in a property of your test class

Cheers!

Reply
Luc H. Avatar

Hi Diego,

Thank you for the tip. For anyone also looking for this, here is a part of my implementation.

 

namespace App\Tests;

use Liip\FunctionalTestBundle\Test\WebTestCase as LiipWebTestCase;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Throwable;

class WebTestCase extends LiipWebTestCase
{
protected static $logDir;


public static function setUpBeforeClass(): void
{
// Create directory where the failing tests write their information

self::$logDir = getcwd()."/var/tests";
if (!is_dir(self::$logDir)) {
mkdir(self::$logDir, 0777, true);
}
}


public function setUp(): void
{
$this->client = null;
}


// This has to be called by the tests to get and store the client

protected function getMyClient(): KernelBrowser
{
$this->client = $this->makeAuthenticatedClient();


return $this->client;
}


protected function onNotSuccessfulTest(Throwable $e): void
{
if ($this->client) {
$this->writeErrorFile($e->getMessage(), $e->getTraceAsString());
}

parent::onNotSuccessfulTest($e);
}


protected function writeErrorFile(string $errorMessage, string $trace): void
{
$testClass = get_class($this);
$testName = $this->getName();
$response = $this->client->getResponse()->getContent();

// Generate a file name containing the test file name and the test name, e.g. App_Tests_Controller_MyControllerTest___testDefault.html
$fileName = str_replace('\\', '_', "$testClass"."___$testName.html");
$content = "<html>$response < pre>Error message: $errorMessage\nFailing test: $testClass::$testName\nStacktrace:\n$trace< /pre></html>";
file_put_contents(self::$logDir."//$fileName", $content);
}
}

(remove the space from < pre> and < /pre>, it breaks the formatting in this board)

2 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the fundamentals of PHPUnit haven't changed, this tutorial *is* built on an older version of Symfony and PHPUnit.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.0, <7.4",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/orm": "^2.5", // v2.7.2
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "sensio/distribution-bundle": "^5.0.19", // v5.0.21
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.28
        "symfony/monolog-bundle": "^3.1.0", // v3.1.2
        "symfony/polyfill-apcu": "^1.0", // v1.6.0
        "symfony/swiftmailer-bundle": "^2.3.10", // v2.6.7
        "symfony/symfony": "3.3.*", // v3.3.13
        "twig/twig": "^1.0||^2.0" // v2.4.4
    },
    "require-dev": {
        "doctrine/data-fixtures": "^1.3", // 1.3.3
        "doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
        "liip/functional-test-bundle": "^1.8", // 1.8.0
        "phpunit/phpunit": "^6.3", // 6.5.2
        "sensio/generator-bundle": "^3.0", // v3.1.6
        "symfony/phpunit-bridge": "^3.0" // v3.4.30
    }
}