Buy Access to Course
12.

Complex Config Test

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

There is one important part of the bundle that is not tested yet: our configuration. If the user sets the min_sunshine option, there's no test that this is correctly passed to the service.

And yea, again, you do not need to have a test for everything: use your best judgment. For configuration like this, there are three different ways to test it. First, you can test the Configuration class itself. That's a nice idea if you have some really complex rules. Second, you can test the extension class directly. In this case, you would pass different config arrays to the load() method and assert that the arguments on the service Definition objects are set correctly. It's a really low-level test, but it works.

And third, you can test your configuration with an integration test like we created, where you boot a real application with some config, and check the behavior of the final services.

If you do want to test the configuration class or the extension class, like always, a great way to do this is by looking at the core code. Press Shift+Shift to open FrameworkExtensionTest. If you did some digging, you'd find out that this test parses YAML files full of framework configuration, parses them, then checks to make sure the Definition objects are correct based on that configuration.

Try Shift + Shift again to open ConfigurationTest. There are a bunch of these, but the one from FrameworkBundle is a pretty good example.

Dummy Test Word Provider

We're going to use the third option: boot a real app with some config, and test the final services. Specifically, I want to test that the custom word_provider config works.

Let's think about this: to create a custom word provider, you need the class, like CustomWordProvider, you need to register it as a service - which is automatic in our app - and then you need to pass the service id to the word_provider option. We're going to do all of that, right here at the bottom of this test class. It's a little nuts, and that's exactly why we're talking about it!

Create a new class called StubWordList and make it implement WordProviderInterface. This will be our fake word provider. Go to the Code -> Generate menu, or Command + N on a Mac, and implement the getWordList() method. Just return an array with two words: stub and stub2.

74 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 2
namespace KnpU\LoremIpsumBundle\Tests;
// ... lines 4 - 66
class StubWordList implements WordProviderInterface
{
public function getWordList(): array
{
return ['stub', 'stub2'];
}
}

Next, copy the testServiceWiring() method, paste it, and rename it to testServiceWiringWithConfiguration(). Remove the last two asserts: we're going to work more on this in a minute.

74 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 12
class FunctionalTest extends TestCase
{
// ... lines 15 - 25
public function testServiceWiringWithConfiguration()
{
$kernel = new KnpULoremIpsumTestingKernel([
'word_provider' => 'stub_word_list'
]);
$kernel->boot();
$container = $kernel->getContainer();
$ipsum = $container->get('knpu_lorem_ipsum.knpu_ipsum');
// ... line 35
}
}
// ... lines 38 - 74

Configuring Bundles in the Kernel

Here's the tricky part: we're using the same kernel in two different tests... but we want them to behave differently. In the second test, I need to pass some extra configuration. This will look a bit technical, but just follow me through this.

First, inside the kernel, go back to the Code -> Generate menu, or Command + N on a Mac, and override the constructor. To simplify, instead of passing the environment and debug flag, just hard-code those when we call the parent constructor.

70 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 38
class KnpULoremIpsumTestingKernel extends Kernel
{
public function __construct()
{
parent::__construct('test', true);
}
// ... lines 45 - 60
}
// ... lines 62 - 70

Thanks to that, we can remove those arguments in our two test functions above. But now, add an optional array argument called $knpUIpsumConfig. This will be the configuration we want to pass under the knpu_lorem_ipsum key.

At the top of the kernel, create a new private variable called $knpUIpsumConfig, and then assign that in the constructor to the argument.

74 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 38
class KnpULoremIpsumTestingKernel extends Kernel
{
private $knpUIpsumConfig;
public function __construct(array $knpUIpsumConfig = [])
{
$this->knpUIpsumConfig = $knpUIpsumConfig;
// ... lines 46 - 47
}
// ... lines 49 - 64
}
// ... lines 66 - 74

Next, find the registerContainerConfiguration() method. In a normal Symfony app, this is the method that's responsible for parsing all the YAML files in the config/packages directory and the services.yaml file.

Instead of parsing YAML files, we can instead put all that logic into PHP with $loader->load() passing it a callback function with a ContainerBuilder argument. Inside of here, we can start registering services and passing bundle extension configuration.

74 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 56
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(function(ContainerBuilder $container) {
// ... lines 60 - 62
});
}
// ... lines 65 - 74

First, in all cases, let's register our StubWordList as a service: $container->register(), pass it any id - like stub_word_list - and pass the class: StubWordList::class. It doesn't need any arguments.

74 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 58
$loader->load(function(ContainerBuilder $container) {
$container->register('stub_word_list', StubWordList::class);
// ... lines 61 - 62
});
// ... lines 64 - 74

Next, we need to pass any custom knpu_lorem_ipsum bundle extension configuration. Do this with $container->loadFromExtension() with knpu_lorem_ipsum and, for the second argument, the array of config you want: $this->knpUIpsumConfig.

74 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 58
$loader->load(function(ContainerBuilder $container) {
// ... lines 60 - 61
$container->loadFromExtension('knpu_lorem_ipsum', $this->knpUIpsumConfig);
});
// ... lines 64 - 74

Basically, each test case can now pass whatever custom config they want. The first won't pass any, but the second will pass the word_provider key set to the service id: stub_word_list.

74 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 12
class FunctionalTest extends TestCase
{
// ... lines 15 - 25
public function testServiceWiringWithConfiguration()
{
$kernel = new KnpULoremIpsumTestingKernel([
'word_provider' => 'stub_word_list'
]);
// ... lines 31 - 35
}
}
// ... lines 38 - 74

The downside of an integration test is that we can't assert exactly that the StubWordList was passed into KnpUIpsum. We can only test the behavior of the services. But since that stub word list only uses two different words, we can reasonably test this with $this->assertContains('stub', $ipsum->getWords(2)).

74 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 25
public function testServiceWiringWithConfiguration()
{
// ... lines 28 - 34
$this->assertContains('stub', $ipsum->getWords(2));
}
// ... lines 37 - 74

Ready to try this? Find your terminal and... run those tests!

./vendor/bin/simple-phpunit

Ah man! Our new test fails! Hmm... it looks like it's not using our custom word provider. Weird!

It's probably weirder than you think. Re-run just that test by passing --filter testServiceWiringWithConfiguration:

./vendor/bin/simple-phpunit --filter testServiceWiringWithConfiguration

It still fails. But now, clear the cache directory:

rm -rf tests/cache

And try the test again:

./vendor/bin/simple-phpunit --filter testServiceWiringWithConfiguration

Holy Houdini Batman! It passed! In fact, try all the tests:

./vendor/bin/simple-phpunit

They all pass! Black magic! What the heck just happened?

When you boot a kernel, it creates a tests/cache directory that includes the cached container. The problem is that it's using the same cache directory for both tests. Once the cache directory is populated the first time, all future tests re-use the same container from the first test, instead of building their own.

It's a subtle problem, but has an easy fix: we need to make the Kernel use a different cache directory each time it's instantiated. There are tons of ways to do this, but here's an easy one. Go back to the Code -> Generate menu, or Command + N on a Mac, and override a method called getCacheDir(). Return __DIR__.'/cache/' then spl_object_hash($this). So, we will still use that cache directory, but each time you create a new Kernel, it will use a different subdirectory.

79 lines | LoremIpsumBundle/tests/FunctionalTest.php
// ... lines 1 - 38
class KnpULoremIpsumTestingKernel extends Kernel
{
// ... lines 41 - 65
public function getCacheDir()
{
return __DIR__.'/cache/'.spl_object_hash($this);
}
}
// ... lines 71 - 79

Clear out the cache directory one last time. Then, run the tests!

./vendor/bin/simple-phpunit

They pass! Run them again:

./vendor/bin/simple-phpunit

You should now see four unique sub-directories inside cache/. I won't do it, but to make things even better, you could clear the cache/ directory between tests with a teardown() method in the test class.