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 SubscribeI also want to test the method that sends the weekly update email. But because the real complexity of this method is centered around generating the PDF, instead of a unit test, let's write an integration test.
In MailerTest
, add a second method: testIntegrationSendAuthorWeeklyReportMessage()
.
... lines 1 - 14 | |
class MailerTest extends TestCase | |
{ | |
... lines 17 - 42 | |
public function testIntegrationSendAuthorWeeklyReportMessage() | |
{ | |
... lines 45 - 58 | |
} | |
} |
Let's start the same way as the first method: copy all of its code except for the asserts, paste them down here and change the method to sendAuthorWeeklyReportMessage()
.
... lines 1 - 14 | |
class MailerTest extends TestCase | |
{ | |
... lines 17 - 42 | |
public function testIntegrationSendAuthorWeeklyReportMessage() | |
{ | |
$symfonyMailer = $this->createMock(MailerInterface::class); | |
$symfonyMailer->expects($this->once()) | |
->method('send'); | |
$pdf = $this->createMock(Pdf::class); | |
$twig = $this->createMock(Environment::class); | |
$entrypointLookup = $this->createMock(EntrypointLookupInterface::class); | |
$user = new User(); | |
$user->setFirstName('Victor'); | |
$user->setEmail('victor@symfonycasts.com'); | |
$mailer = new Mailer($symfonyMailer, $twig, $pdf, $entrypointLookup); | |
$email = $mailer->sendWelcomeMessage($user); | |
} | |
} |
This needs a User
object... but it also needs an array of articles. Let's create one: $article = new Article()
. These articles are passed to the template where we print their title. So let's at least populate that property: $article->setTitle()
:
Black Holes: Ultimate Party Pooper
... lines 1 - 4 | |
use App\Entity\Article; | |
... lines 6 - 14 | |
class MailerTest extends TestCase | |
{ | |
... lines 17 - 42 | |
public function testIntegrationSendAuthorWeeklyReportMessage() | |
{ | |
... lines 45 - 52 | |
$user = new User(); | |
$user->setFirstName('Victor'); | |
$user->setEmail('victor@symfonycasts.com'); | |
$article = new Article(); | |
$article->setTitle('Black Holes: Ultimate Party Pooper'); | |
... lines 58 - 60 | |
} | |
} |
Use this for the 2nd argument of sendAuthorWeeklyReportMessage()
: an array with just this inside.
... lines 1 - 14 | |
class MailerTest extends TestCase | |
{ | |
... lines 17 - 42 | |
public function testIntegrationSendAuthorWeeklyReportMessage() | |
{ | |
... lines 45 - 52 | |
$user = new User(); | |
$user->setFirstName('Victor'); | |
$user->setEmail('victor@symfonycasts.com'); | |
$article = new Article(); | |
$article->setTitle('Black Holes: Ultimate Party Pooper'); | |
$mailer = new Mailer($symfonyMailer, $twig, $pdf, $entrypointLookup); | |
$email = $mailer->sendAuthorWeeklyReportMessage($user, [$article]); | |
} | |
} |
It's time to think strategically about our mocks. Right now, every dependency is mocked, which means it's a pure unit test. If we kept doing this, we could probably make sure that whatever render()
returns is passed to the PDF function... and even assert that whatever that returns is passed to the attach()
method. It's not bad, but because the logic in this method isn't terribly complex, its usefulness is limited.
What really scares me is the PDF generation: does my Twig template render correctly? Does the PDF generation process work... and do I really get back PDF content? To test this, instead of mocking $twig
and $pdf
, we could use the real objects. That would make this an integration test. These are often more useful than unit tests... but are also much slower to run, and it will mean that I really do need to have wkhtmltopdf
installed on this machine, otherwise my tests will fail. Tradeoffs!
So here's the plan: use the real $twig
and $pdf
objects but keep mocking $symfonyMailer
and $entrypointLookup
... because I don't really want to send emails... and the $entrypointLookup
doesn't matter unless I want to test that it does reset things correctly between rendering 2 PDFs.
To make this test able to use real objects, we need to change extends
from TestCase
to KernelTestCase
.
... lines 1 - 9 | |
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
... lines 11 - 15 | |
class MailerTest extends KernelTestCase | |
{ | |
... lines 18 - 62 | |
} |
That class extends the normal TestCase
but gives us the ability to boot Symfony's service container in the background. Specifically, it gives us the ability, down in the method, to say: self::bootKernel()
.
... lines 1 - 9 | |
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
... lines 11 - 15 | |
class MailerTest extends KernelTestCase | |
{ | |
... lines 18 - 43 | |
public function testIntegrationSendAuthorWeeklyReportMessage() | |
{ | |
self::bootKernel(); | |
$symfonyMailer = $this->createMock(MailerInterface::class); | |
... lines 48 - 61 | |
} | |
} |
That will give us the ability to fetch real service objects and use them.
So we'll leave $symfonyMailer
mocked, leave the $entrypointLookup
mocked, but for the Pdf
, get the real Pdf
service. How? In the test environment, we can fetch things out of the container using the same type-hints as normal. So, $pdf = self::$container
- bootKernel()
set that property - ->get()
passing this Pdf::class
. Do the same for Twig: self::$container->get(Environment::class)
.
Tip
Starting in Symfony 5.3, instead of self::$container
, use static::getContainer()
to get the container
from inside a test. Also, calling bootKernel()
is no longer needed.
... lines 1 - 9 | |
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
... lines 11 - 15 | |
class MailerTest extends KernelTestCase | |
{ | |
... lines 18 - 43 | |
public function testIntegrationSendAuthorWeeklyReportMessage() | |
{ | |
self::bootKernel(); | |
... lines 47 - 49 | |
$pdf = self::$container->get(Pdf::class); | |
$twig = self::$container->get(Environment::class); | |
... lines 52 - 61 | |
} | |
} |
I love that! Again, the downside is that you really do need to have wkhtmltopdf
installed correctly anywhere you run your tests. That's the cost of doing this.
Before we try it, at the bottom, we don't have any asserts yet. Let's add at least one: $this->assertCount()
that 1 is the count of $email->getAttachments()
.
... lines 1 - 15 | |
class MailerTest extends KernelTestCase | |
{ | |
... lines 18 - 43 | |
public function testIntegrationSendAuthorWeeklyReportMessage() | |
{ | |
... lines 46 - 60 | |
$email = $mailer->sendAuthorWeeklyReportMessage($user, [$article]); | |
$this->assertCount(1, $email->getAttachments()); | |
} | |
} |
We could go further and look closer at the attachment... maybe make sure that it looks like it's in a PDF format... but this is a good start.
Now let's try this. Find your terminal and run our normal:
php bin/phpunit
It is slower this time... and then.. ah! What just happened? Two things. First, because this booted up a lot more code, we're seeing a ton of deprecation warnings. These are annoying... but we can ignore them.
The second thing is that... the test failed! But... weird - not how I expected: something about APCu is not enabled. Huh? Why is it suddenly trying to use APCu?
The cause of this is specific to our app... but it's an interesting situation. Open up config/packages/cache.yaml
.
framework: | |
cache: | |
... lines 3 - 14 | |
app: '%cache_adapter%' | |
... lines 16 - 21 |
See this app
key? This is where you can tell Symfony where it should store things that need to be added to cache at runtime - like the filesystem, redis or APCu. In an earlier tutorial, we set this to a parameter that we invented: %cache_adapter%
.
This allows us to do something cool. Open config/services.yaml
.
... lines 1 - 5 | |
parameters: | |
cache_adapter: cache.adapter.apcu | |
... lines 8 - 53 |
Here, we set cache_adapter
to cache.adapter.apcu
: we told Symfony to store cache in APCu. And... apparently, I don't have that extension installed on my local machine.
Ok... fine... but then... how the heck is the website working? Shouldn't we be getting this error everywhere? Yep... except that we override this value in services_dev.yaml
- a file that is only loaded in the dev
environment. Here we tell it to use cache.adapter.filesystem
.
parameters: | |
cache_adapter: 'cache.adapter.filesystem' |
This is great! It means that we don't need any special extension for the cache system while developing... but on production, we use the superior APCu.
The problem now is that, when we run our tests, those are run in the test
environment... and since the test
environment doesn't load services_dev.yaml
, it's using the default APCu adapter! By the way, there is a services_test.yaml
file... but it has nothing in it. In fact, you can delete this: it's for a feature that's not needed anymore.
So, honestly... I should have set this all up better. And now, I will. Change the default cache adapter to cache.adapter.filesystem
.
... lines 1 - 5 | |
parameters: | |
cache_adapter: cache.adapter.filesystem | |
... lines 8 - 53 |
Then, only in the prod
environment, let's change this to apcu
. To do that, rename services_dev.yaml
to services_prod.yaml
... and change the parameter inside to cache.adapter.apcu
.
parameters: | |
cache_adapter: 'cache.adapter.apcu' |
Now the test
environment should use the filesystem. Let's try it!
php bin/phpunit
And... if you ignore the deprecations... it worked! It actually generated the PDF inside the test! To totally prove it, real quick, in the test, var_dump($email->getAttachments())
... and run the test again:
php bin/phpunit
Yea! It's so ugly. The attachment is some DataPart
object and you can see the crazy PDF content inside. Go take off that dump.
Ok, the last type of test is a functional test. And this is where things get more interesting... especially in relation to Mailer. If we want to make a functional test for the registration form... do we expect our test to send a real email? Or should we disable email delivery somehow while testing? And, in both cases, is it possible to submit the registration form in a functional test and then assert that an email was in fact sent? Ooo. This is good stuff!
Found solution
self::bootKernel();
$container = static::getContainer();
$mailerMock = $this->createMock(MailerInterface::class);
$entity = $container->get(EntityManagerInterface::class);
$twig = $container->get(Environment::class);
$pdf = $container->get(Pdf::class);
$entryLookupMock = $this->createMock(EntrypointLookupInterface::class);
Hey Mepcuk,
I'm happy you found the solution! And thanks for sharing it with others! It seems like you need to use the method instead of the property now.
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"aws/aws-sdk-php": "^3.87", // 3.110.11
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-snappy-bundle": "^1.6", // v1.6.0
"knplabs/knp-time-bundle": "^1.8", // v1.9.1
"league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
"league/flysystem-cached-adapter": "^1.0", // 1.0.9
"league/html-to-markdown": "^4.8", // 4.8.2
"liip/imagine-bundle": "^2.1", // 2.1.0
"nexylan/slack-bundle": "^2.1,<2.2.0", // v2.1.0
"oneup/flysystem-bundle": "^3.0", // 3.1.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.4.1
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.3.4
"symfony/console": "^4.0", // v4.3.4
"symfony/flex": "^1.9", // v1.17.6
"symfony/form": "^4.0", // v4.3.4
"symfony/framework-bundle": "^4.0", // v4.3.4
"symfony/mailer": "4.3.*", // v4.3.4
"symfony/messenger": "4.3.*", // v4.3.4
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.3.4
"symfony/sendgrid-mailer": "4.3.*", // v4.3.4
"symfony/serializer-pack": "^1.0", // v1.0.2
"symfony/twig-bundle": "^4.0", // v4.3.4
"symfony/twig-pack": "^1.0", // v1.0.0
"symfony/validator": "^4.0", // v4.3.4
"symfony/web-server-bundle": "^4.0", // v4.3.4
"symfony/webpack-encore-bundle": "^1.4", // v1.6.2
"symfony/yaml": "^4.0", // v4.3.4
"twig/cssinliner-extra": "^2.12", // v2.12.0
"twig/extensions": "^1.5", // v1.5.4
"twig/inky-extra": "^2.12" // v2.12.0
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.2.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/browser-kit": "4.3.*", // v4.3.5
"symfony/debug-bundle": "^3.3|^4.0", // v4.3.4
"symfony/dotenv": "^4.0", // v4.3.4
"symfony/maker-bundle": "^1.0", // v1.13.0
"symfony/monolog-bundle": "^3.0", // v3.4.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.4
"symfony/profiler-pack": "^1.0", // v1.0.4
"symfony/var-dumper": "^3.3|^4.0" // v4.3.4
}
}
$pdf = self::$container->get(Pdf::class);
does not work on Symfony 6