If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeDespite 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 | |
} | |
} |
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!
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!
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.
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!
I know, it's a great feature, but on this page the template cannot be expanded fully. At least I cannot do it.
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!
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?
Hey Luc H.
I think you need to hold the client and/or response instance in a property of your test class
Cheers!
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)
// 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
}
}
I think there is only a part of index.html.twig in the script.