Complex Config Test
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThere 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
.
// ... 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.
// ... 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.
// ... 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.
// ... 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.
// ... 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.
// ... 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
.
// ... 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
.
// ... 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))
.
// ... 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.
// ... 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.
Getting stuck in solving the following problem.
I’m building an application which accepts a command from the front-end and “broadcasts” it to all the services available. Simple as that. Services mentioned are supposed to be an access layer for third-party REST APIs.
I would like to implement each service as a separate bundle which in its turn must implement one of my “internal” contracts/interfaces. Hope it sounds reasonable as in this case I could just give my contracts to outsourcing developers not allowing them inside my main application.
I don’t want to restrict each bundle in its own dependencies. I just want them to implement my interface(s). But let’s assume that one of those bundles would like to use
symfony/http-client
as a dependency to talk to a third-party REST API. Symfony HttpClient Component is rather good (no need to use Guzzle, etc.) and moreover supportsscoped_clients
. Got the idea? I have a dependency of Symfony HttpClient in my main application and would like that each bundle could configure it for yourself. Thus I would like to set up that scoped HttpClient straight in@MyBundle/Resources/config/services.xml
. Does it violate any reasonable logic of the Symfony framework? Is this allowed? If so what I’m doing wrong if I get an error while putting something like this https://gist.github.com/erop/44dc45aee6f7c7c47b9d50bc7e5bf714 . The error message is ``In XmlFileLoader.php line 681:There is no extension able to load the configuration for "framework:config" (in /app/libs/<bundle_dir>/src/DependencyInjection/../Resources/config/services.xml). Looked for namespace "http://symfony.com/schema/dic/symfony", found none `` This is the first point I’m getting stuck.
Looks like I miss some concepts of service container. Or I’m doing something wrong to properly set it up. Could someone give a direction for solving the issue?