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 SubscribeThanks to the unit test, we can confidently say that the KnpUIpsum
class works correctly. But... that's only like 10% of our bundle's code! Most of the bundle is related to service configuration. So what guarantees that the bundle, extension class, Configuration class and services.xml
files are all correct? Nothing! Yay!
And it's not that we need to test everything, but it would be great to at least have a "smoke" test that made sure that the bundle correctly sets up a knpu_lorem_ipsum.knpu_ipsum
service.
We're going to do that with a functional test! Or, depending on how you name things, this is really more of an integration test. Details. Anyways, in the tests/
directory, create a new class called FunctionalTest
.
Make this extend the normal TestCase
from PHPUnit, and add a public function testServiceWiring()
.
... lines 1 - 8 | |
class FunctionalTest extends TestCase | |
{ | |
public function testServiceWiring() | |
{ | |
} | |
} | |
... lines 16 - 27 |
And here is where things get interesting. We basically want to initialize our bundle into a real app, and check that the container has that service. But... we do not have a Symfony app lying around! So... let's make the smallest possible Symfony app ever.
To do this, we just need a Kernel class. And instead of creating a new file with a new class, we can hide the class right inside this file, because it's only needed here.
Add class KnpULoremIpsumTestingKernel extends Kernel
from... wait... why is this not auto-completing the Kernel
class? There should be one in Symfony's HttpKernel component! What's going on?
Remember! In our composer.json
, other than the PHP version, the require
key is empty! We're literally saying that someone is allowed to use this bundle even if they use zero parts of Symfony. That's not OK. We need to be explicit about what dependencies are actually required to use this bundle.
But... what dependencies are required, exactly? Honestly... most bundles simply require symfony/framework-bundle
. FrameworkBundle provides all of the core services, like the router, session, etc. It also requires the http-kernel
component, event-dispatcher
and probably anything else that your bundle relies on.
Requiring FrameworkBundle is not a horrible thing. But, it's technically possible to use the Symfony framework without the FrameworkBundle, and some people do do this.
So we're going to take the tougher, more interesting road and not simply require that bundle. Instead, let's look at the actual components our code uses. For example, open the bundle class. Obviously, we depend on the http-kernel
component. And in the extension class, we're using config
and dependency-injection
. In Configuration
, nothing new: just config
.
Ok! Our bundle needs the config
, dependency-injection
and http-kernel
components. And by the way, this is exactly why we're writing the integration test! Our bundle is not setup correctly right now... but it wasn't very obvious.
In composer.json
, add these: symfony/config
at version ^4.0
. Copy this and paste it two more times. Require symfony/dependency-injection
and symfony/http-kernel
.
... lines 1 - 11 | |
"require": { | |
... line 13 | |
"symfony/config": "^4.0", | |
"symfony/dependency-injection": "^4.0", | |
"symfony/http-kernel": "^4.0" | |
}, | |
... lines 18 - 32 |
Now, find your terminal, and run:
composer update
Perfect! Once that finishes, we can go back to our functional test. Re-type the "l" on Kernel
and... yes! There is the Kernel class from http-kernel
.
This requires us to implement two methods. Go to the Code -> Generate menu - or Command + N on a Mac - click "Implement Methods" and choose the two.
... lines 1 - 2 | |
namespace KnpU\LoremIpsumBundle\Tests; | |
... lines 4 - 16 | |
class KnpULoremIpsumTestingKernel extends Kernel | |
{ | |
public function registerBundles() | |
{ | |
} | |
public function registerContainerConfiguration(LoaderInterface $loader) | |
{ | |
} | |
} |
Inside registerBundles
, return an array and only enable our bundle: new KnpULoremIpsumBundle()
. Since we're not dependent on any other bundles - like FrameworkBundle
- we should, in theory, be able to boot an app with only this. Kinda cool!
... lines 1 - 26 | |
public function registerBundles() | |
{ | |
return [ | |
new KnpULoremIpsumBundle(), | |
]; | |
} | |
... lines 33 - 38 |
And... that's it! Our app is ready. Back in testServiceWiring
, add $kernel = new KnpULoremIpsumTestingKernel()
and pass this test
for the environment, thought that doesn't matter, and true
for debug. Next, boot the kernel, and say $container = $kernel->getContainer()
.
... lines 1 - 10 | |
class FunctionalTest extends TestCase | |
{ | |
public function testServiceWiring() | |
{ | |
$kernel = new KnpULoremIpsumTestingKernel('test', true); | |
$kernel->boot(); | |
$container = $kernel->getContainer(); | |
... lines 18 - 21 | |
} | |
} | |
... lines 24 - 38 |
This is great! We just booted a real Symfony app. And now, we can makes sure our service exists. Add $ipsum = $container->get()
, copy the id of our service, and paste it here. We can do this because the service is public.
Let's add some very basic checks, like $this->assertInstanceOf()
that KnpUIpsum::class
is the type of $ipsum
. And also, $this->assertInternalType()
that a string is what we get back when we call $ipsum->getParagraphs()
.
... lines 1 - 12 | |
public function testServiceWiring() | |
{ | |
... lines 15 - 18 | |
$ipsum = $container->get('knpu_lorem_ipsum.knpu_ipsum'); | |
$this->assertInstanceOf(KnpUIpsum::class, $ipsum); | |
$this->assertInternalType('string', $ipsum->getParagraphs()); | |
} | |
... lines 23 - 38 |
The unit test truly tests this class - so we really only need a sanity check. I think it's time to try this! Find your terminal, and run:
./vendor/bin/simple-phpunit
Yes! We're now sure that our service is wired correctly! So, this functional test didn't fail like I promised in the last chapter. But the point is this: before we added our dependencies, our bundle was not actually setup correctly.
And, woh! In the tests/
directory, we suddenly have a cache/
folder! That comes from our kernel: it caches files just like a normal app. To make sure this doesn't get committed, open .gitignore
and ignore /tests/cache
.
... lines 1 - 3 | |
/tests/cache |
Next, let's get a little more complex by testing that some of our configuration options work.
Hey Saif!
Could you elaborate on this? What question do you have? I only see the dumped PHPUnit output.
Cheers!
Hey Saif,
Is the error resolved? If so, that's awesome! I'm happy you were able to solve the problem yourself, well done! If you want to share the actual solution with others - feel free to do this in a comment :)
Cheers!
Hey Victory
The actual problem of the code in TreeBuilder.
in this video the code is given
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->getRootNode('knpu_lorem_ipsum');```
and i was trying test the code with functional test its gave error:
<blockquote>1x: A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0.
1x in FunctionalTest::testServiceWiring from KnpU\LoremIpsumBundle\Tests
</blockquote>
but i modified the code like
$treeBuilder = new TreeBuilder('knpu_lorem_ipsum');
$rootNode = method_exists(TreeBuilder::class, 'getRootNode') ? $treeBuilder->getRootNode() : $treeBuilder->root('knpu_lorem_ipsum');```
and its now working fine.
thanks Victor
Hey Saif Ali,
Ah, good catch! Yeah, that part changed in the newer version. We had a not about it I think, in this specific chapter: https://symfonycasts.com/sc...
Anyway, thank you for sharing the final solution with others!
Cheer!
Hi, really geat tutorial. When I run simple-phpunit a recive a message "no tests executed".
Any Idea.
thks.
Xaver
Is your test folder and files structure correct? I mean, all the test files must live inside tests
and should be post-fixed with Test
, and your test methods should start with the word test
(lowercase). Also, double-check that your phpunit.xml file is correct
Cheers!
Hi, thanks for the answer.
I've checked all the code again and seems everything okay to me.
I've run ./vendor/bin/simple-phpunit src/tests/FunctionalTest.php. The Test runs bun receive some warnings:
PHPUnit 8.3.5 by Sebastian Bergmann and contributors.
Testing KnpU\LoremIpsumBundle\tests\FunctionalTest
W 1 / 1 (100%)
Time: 59 ms, Memory: 8.00 MB
There was 1 warning:
1) KnpU\LoremIpsumBundle\tests\FunctionalTest::testServiceWiring
assertInternalType() is deprecated and will be removed in PHPUnit 9. Refactor your test to use assertIsArray(), assertIsBool(), assertIsFloat(), assertIsInt(), assertIsNumeric(), assertIsObject(), assertIsResource(), assertIsString(), assertIsScalar(), assertIsCallable(), or assertIsIterable() instead.
WARNINGS!
Tests: 1, Assertions: 2, Warnings: 1.
Remaining direct deprecation notices (2)
1x: A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0.
1x in FunctionalTest::testServiceWiring from KnpU\LoremIpsumBundle\tests
1x: The "Symfony\Component\Config\Definition\Builder\TreeBuilder::root()" method called for the "knpu_lorem_ipsum" configuration is deprecated since Symfony 4.3, pass the root name to the constructor instead.
1x in FunctionalTest::testServiceWiring from KnpU\LoremIpsumBundle\tests.
Well I don't understand why the will be used the 8.3.5 version of PHPUnit even in composer.json I have
"require-dev": {
"symfony/phpunit-bridge": "^5.2"
}
Thank you.
Well, at least it's detecting your tests now. The symfony/phpunit-bridge
library is just a helper package to install all the libraries you will need to integrate PHPUnit with your Symfony app. The way to control the version of PHPUnit is through the phpunit.xml
file, if you open it up you should find a line like this one
// phpunit.xml
<php>
<server name="SYMFONY_PHPUNIT_VERSION" value="8.3" />
</php>
if you want to use a different version just change that value and the next time you run PHPUnit, it should install that version
Cheers!
Hey @there
First, thank for such a great tutorial, I have a question about configuration and extension class, why we still validating service config YAML file when it does not exist in bundle project self when I dump() after getDefinition() $configs is empty but test still working. I'm confused, thanks
Hey @someGuest!
Sorry for my SUPER slow reply - we hit SymfonyWorld conference week... and everything went crazy.
I have a question about configuration and extension class, why we still validating service config YAML file when it does not exist in bundle project self when I dump() after getDefinition() $configs is empty but test still working.
In our test, you're correct that i've chosen to not to pass any config to my bundle - it's kind of like testing what would happen if someone used my bundle with no YAML config file. We could have passed config to the bundle if we wanted to in the test like this:
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(function (ContainerBuilder $container) use ($loader) {
$container->loadFromExtension('knpu_lorem_ipsum', [
// array version of your YAML config
]);
}
Anyways, we didn't pass any config. This means, as you saw, that $configs
is empty inside our extension class. This empty array is then passed to our Configuration class, and THAT adds any "defaults" that we've set up. For example, after the Configuration class is processed, the $config
variable will still have a key with unicorns_are_real
set to true - https://symfonycasts.com/screencast/symfony-bundle/bundle-configuration#codeblock-3c45ce51e5
Let me know if that helps explain things!
Cheers!
Hm-m-m.... What if I need to use "core" app services in my bundle? I extracted interfaces from them (some helper services, entity repositories, etc.) and put in a separate Composer package <my_verndor_name>/contracts to be required both in core app and the bundle. Now I try to complete instructions given in the video and receive an error while static::createClient()
in my functional test:Symfony\Component\DependencyInjection\Exception\RuntimeException : Cannot autowire service "bundle_controller_service_id": argument "$eventStore" of method "My\Bundle\Namespace\CallbackController::__construct()" references interface "Vendor\Contracts\BundleEventStoreInterface" but no such service exists. Did you create a class that implements this interface?<br />
I understand the cause of an error. But what approach should be applied for testing bundles in this case?
Hey erop
It's again me =) I'm not sure because I don't see full picture of what are you doing, but I guess you should require your bundle as dev dependency. And of course for bundles is better to not use autowiring, but fully configure every service
Cheers!
Hey sadikoff !
Yeah, I got your point about not using autowiring. OK, let's throw it away for now... Will try to explain in other words... I have only an interface of the real "core" service/repository at the bundle's side. This interface was just extracted with PhpStorm and put in a separate Composer package which in its turn required both by the "core" app and the bundle. OK, now let's start writing bundle's services.xml... There is the bundle's service class with arguments in constructor. I write <argument type="service" id="Vendor\Contracts\MyCoreAppServiceInterface"/>
. So far so good... Now I need to define <b>concrete</b> class for this interface in the <service> element of bundle's services.xml. But it's impossible as real class lives in the "core" app. At the test level I could mock "core" app service in the unit tests. But what if I need functional tests? With autowiring enabled in the bundle the whole app works perfectly since I aliased interface for real class in "core" app' services.yaml. OK, no autowiring from now. But how to properly setup bundle's services.xml in this case? Does the whole idea with extracted interfaces for usage in bundles make sense?
Hey erop
Sorry for so late answer. IIRC if you are testing a bundle, you should instatiate each service which you need for it and pass, you can't just wire 'em in config, and then run test suite. That's why you will need add core to dev dependencies to your bundles. So in that case I don't see benefit of using extracted from core interfaces. Probably it works only if you extract interfaces from bundles.
Cheers. Hope my answer is clear enough =)
Hi!
If in configuration, an element is marked as required, like:
->scalarNode(Parameters::SECRET_KEY)->isRequired()
Running the tests throw an error of type "The child node "secret_key" at path "knpu_lorem_ipsum" must be configured".
After a few tries, it can be solved by filling the registerContainerConfiguration function with something like:
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getProjectDir().'/tests/config/test.yaml');
}
But despite having my environment variables declared in my phpunit.xml:
<php>
<env name="SECRET_KEY" value="blibloubliblou123456" force="true"/>
</php>
They do not seem to be loaded.
I've seen that there was some kind of hot debate around a similar topic:
https://github.com/symfony/flex/issues/251
Is there a best practice or a consensus? Thanks a lot!
Edit3: finally got it, posting my code in case it helps others:
<?php
namespace KnpU\LoremIpsum\Tests;
use KnpU\LoremIpsum\KnpULoremIpsum;
use KnpU\LoremIpsum\DependencyInjection\Parameters;
use KnpU\LoremIpsum\KnpUIpsum;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;
class FunctionalTest extends TestCase
{
public function testServiceWiring()
{
$kernel = new KnpULoremIpsumTestingKernel('test', true);
$kernel->boot();
$container = $kernel->getContainer();
$ipsum = $container->get('knpu_lorem_ipsum.knpu_ipsum');
$this->assertInstanceOf(KnpUIpsum::class, $ipsum);
}
}
class KnpULoremIpsumTestingKernel extends Kernel
{
public function registerBundles()
{
return [
new KnpULoremIpsum()
];
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(static function (ContainerBuilder $container) {
$container->setParameter('knpu_lorem_ipsum_secret_key', $_ENV['SECRET_KEY']);
});
$loader->load($this->getProjectDir().'/tests/config/knpu_lorem_ipsum.yaml');
}
}
My env var SECRET_KEY is defined in phpunit.xml
the knpu_lorem_ipsum.yaml files defines secret_key like this:
knpu_lorem_ipsum:
secret_key: '%knpu_lorem_ipsum_secret_key%'
The thing I didn't understand immediately is that setParameter defines a parameter var, not an env var, so by pointing to a parameter var in the yaml file and definin that parameter var with the phpunit env var in the container configuration, I was finally able to make it work. Don't know at all if it's optimal, but doubt it ^^
Hey Renaud G.!
Hmm, let's see if we can pull all of this apart :). It seems to me (but correct me if I'm wrong!) That your intention is to:
A) Allow your bundle to have a secret_key
configuration option
B) Allow a user to use an environment variable to set this value
If so, then, forget about the integration test for a moment. If I were going to use your bundle in my app, and I wanted to use an environment variable to set this key, my config file would look like this:
knpu_lorem_ipsum:
secret_key: '%env(SECRET_KEY)%'
This is the syntax in config files for reading environment variables. Now, a SECRET_KEY
in my .env file would automatically be used.
Now, to the integration test! I had to read those old issues to remember where we "landed" on all of this stuff :). First, in your tests/config/knpu_lorem_ipsum.yaml
, I would reference an environment variable using the syntax above. If you do this, then I think (full disclosure: I can't remember for sure) it will just work: your SECRET_KEY from phpunit.xml will become an environment variable and Symfony will... just read it!
But, let me know if that's not true, or if I've missed the point entirely ;).
Cheers!
Hi, I was getting following error
`1) KnpU\LoremIpsumBundle\Tests\FunctionalTest::testServiceWiring
TypeError: Argument 1 passed to KnpU\LoremIpsumBundle\KnpUIpsum::__construct() must be of the type array, object given, called in /var/www/html/symfony/LoremIpsumBundle/tests/cache/000000005cb2b796000000006d90088b/ContainerGFPl0u4/getKnpuLoremIpsum_KnpuIpsumService.php on line 9`
I had to add below code to the Bundle Class.
`public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new WordProviderCompilerPass());
}`
You haven't talked about this in the video. Can you please explain a bit?
Hey Dushyant,
Could you provide some steps to reproduce so we could take a look? Did you download the course code? Are you working in start/ or finish/ directory? What do you do when you see this error? Some steps to reproduce the error you are talking about would help a lot.
Cheers!
Hi Victor, Thank you.
The issue is that I haven't come across the function public function build(ContainerBuilder $container
in class KnpULoremIpsumBundle
in these videos. Can you please share where did you explaine this?
So I find it difficult to forward.
Hey Dushyant,
Ah, sure! We create that bundle class, i.e. KnpULoremIpsumBundle in the 2nd chapter: https://symfonycasts.com/sc... . And later in https://symfonycasts.com/sc... we show how to load bundle's extension. Did you want the course from scratch btw?
I hope this helps!
Cheers!
Thank you Victor for the response. I will again try the course from scratch after finishing other course from SC.
Hey Dushyant,
OK, let us know if you still have some misunderstanding and we will try to help!
Cheers!
Hey Dushyant,
Ah, thanks for the kind words about our screencasts! It always pushes us forward to make new awesome screencasts ;)
Cheers!
The screencasts are proving to be invaluable. Fabien's book proved to be a useful overview of Symfony 5 (I'm starting fresh, moving a mature non-framework project into Symfony), but the minimalist approach does not go far enough (nor did it intend to). It's working through the screencasts, but using Symfony 5, that's showing me how to code in the current Symfony ecosystem.
Welcome to Symfony Edward B.! You're 100% right about Fabien's book - I found it *fascinating* - but happy that the screencasts are filling in the details :).
Thanks for dropping the nice comment!
Just a notice:
assertInternalType()
is set as deprecated and shows warnings.
but you can simply use assertTrue(is_string($value))
Hey Maik,
Yep, you can! Or you can use a specific method for this, e.g. "assertIsString()". Actually, there are much more useful method that provide you similar checks, check this list for more information: https://github.com/sebastia...
Cheers!
My service is dependent on the security.token_storage service and therefore the bundle requires the SecurityBundle. When I made my testing kernel, I got an error that my service was dependent on a non-existent service "security.token_storage". I added the security bundle to my registered bundles array, but I still get the error. Is there any way that I can test this correctly?
Hey David L. ,
Hm, are you sure you enabled the bundle properly in bundles.php? And is this enabled for all the environments? Could you clear the cache and try again? Do you still see the same error? If so, could you show the exact error?
Cheers!
I ended up needing to load the framework bundle and a default configuration before my kernel would boot and make the service available:
class MyTestingKernel extends Kernel
{
use MicroKernelTrait;
public function registerBundles()
{
return [
new FrameworkBundle(),
new SecurityBundle(),
new MyBundle()
];
}
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
{
$loader->load(__DIR__.'/Functional/app/config/config.yml');
}
protected function configureRoutes(RouteCollectionBuilder $routes)
{
}
}
Hey David L. ,
I'm glad you was able to solve this! And thank you for sharing your solution with others.
Cheers!
Hey Carlos,
There's a way to test it. You can create a file e.g. services_test.yaml and include it only for test env. In that class you can add aliases, something like "test_App\Service\YourServiceName" and add a global config to make all those aliases public in that file. So, this way you can easily fetch private services in test env because we have their public aliases now.
If you need more detailed example, let me know.
Cheers!
Victor,
can you please elaborate on testing private services? I tried to find some documentation on Symfony docs with no apparent success. Detailed example you offered to provide would be of help for me.
Regarding your offered solution, where should the "services_test.yaml" be preferably located (file structure for functional tests), what shall I do to "include" it, how to create an alias for our "knpu_lorem_ipsum.knpu_ipsum" service, and how to add a "global" config to declare those aliases public?
Thanks.
Hey Voltel,
It doesn't matter, but I'd recommend you to create that "services_test.yaml" in your config/ dir where you already have "services.yaml". In it, you need to make public all your private services you want to test, something like:
services:
App\Service\YourServiceName:
public: true
Or you can use some special alias for this, e.g:
services:
test.App\Service\YourServiceName:
public: true
Then in test code you will need to call that service by "->get('test.'.\App\Service\YourServiceName::class)". Also, take a look at docs about how to access the container: https://symfony.com/doc/current/testing.html#accessing-the-container .
It depends on your Kernel, but by default Symfony should load the "services_%env%.yaml" file itself if it exist, so you don't need to do anything to load this file manually, it should be loaded automatically. What you want to do is probably include all the content of "services_dev.yaml" file inside your "services_test.yaml", i.e.:
# config/services_test.yaml
imports:
- { resource: services_dev.yaml }
services:
# Declare all your private services you want to test as public here
I hope this helps!
Cheers!
Sorry, I obviously wasn't explicit enough with my question. In the video of this screencast (starting from minus 2:10), Ryan talks about this: "Next, boot the kernel, and say $container = $kernel->getContainer()."
So, he creates a mini-app right inside the bundle testing suite. And this mini-app (basically, just a booted "KnpULoremIpsumTestingKernel") is responsible for loading the tested bundle. So, there is no "services.yaml" file in the bundle or bundle testing suite. We have a "src/Resources/config/services.xml" file with definition that makes our bundle's primary service private:
<service id="knpu_lorem_ipsum.knpu_ipsum" class="KnpU\LoremIpsumBundle\KnpUIpsum" public="false" >
</service>
The question remains: the service is declared private in the bundle configuration. How can we test the bundle within the bundle's "tests/" folder to make sure the "knpu_lorem_ipsum.knpu_ipsum" service is present?
Currently, I have tried the following checks with no success:
// in tests/FunctionalTest.php
class FunctionalTest extends WebTestCase
{
public function testServiceWiring()
{
$kernel = new KnpULoremIpsumTestingKernel();
$kernel->boot();
$container = $kernel->getContainer();
$c_main_service = 'knpu_lorem_ipsum.knpu_ipsum';
if ($container->has('test.service_container')) {
/** @var Container $test_container */
$test_container = $container->get('test.service_container');
$ipsum = $test_container->get($c_main_service);
} elseif (!empty(self::$container)) {
$test_container = self::$container;
$ipsum = $test_container->get($c_main_service);
} else if ($container->has($c_main_service)) {
$ipsum = $container->get($c_main_service);
} else {
throw new \LogicException(sprintf('Cannot get private service "%s" from the container. ', $c_main_service));
}//endif
//$ipsum = $container->get('Test\App\Service\KnpUIpsum')
$this->assertInstanceOf(KnpUIpsum::class, $ipsum);
$c_text = $ipsum->getParagraphs();
$this->assertIsString($c_text);
$this->assertStringContainsString('stub', $c_text);
}
}
My <a href="https://github.com/voltel/knpu-lorem-ipsum-bundle">repo with the screecast code is at https://github.com/voltel/knpu-lorem-ipsum-bundle</a>, but notice that currently the "knpu_lorem_ipsum.knpu_ipsum" is declared public="true" in "src/Resources/config/services.xml". Please, change to public="false" and modify the tests in the "tests/FunctionalTest.php" for them to pass.
============================================
UPDATE: I modified the load callback inside the "KnpULoremIpsumTestingKernel::registerContainerConfiguration()" method:
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(function (ContainerBuilder $container_builder) use ($loader) {
// This fake service id is used in "testServiceWiringWithConfiguration" and is set here:
$container_builder->register('fake_word_provider', FakeWordProvider::class)
->addTag('knpu_lorem_ipsum_word_provider');
$container_builder->loadFromExtension('knpu_lorem_ipsum', $this->aKnpuLoremIpsumConfig);
// This is what I added to make "knpu_lorem_ipsum.knpu_ipsum" service,
// which I declared "private" in "src/Resource/config/services.xml", "public" and "visible" during the testing.
$container_builder->addCompilerPass(new CustomCompilerPass());
});
}//end of function
Then, I also introduced in the same file a new class - "CustomCompilerPass":
class CustomCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container_builder)
{
$definition = $container_builder->getDefinition('knpu_lorem_ipsum.knpu_ipsum');
$definition->setPublic(true);
}//end of function
}
Now, in the "testServiceWiring()", I can assert get the service from the container and assert its class:
$kernel = new KnpULoremIpsumTestingKernel();
$kernel->boot();
$container = $kernel->getContainer();
$ipsum = $container->get(self::MAIN_BUNDLE_SERVICE);
$this->assertInstanceOf(KnpUIpsum::class, $ipsum);
// ...
Hey @voltel!
This is an excellent question! And I like your solution - very clever ;).
I can offer one simplification - and this is what I do. In my kernel class (in configure Container() if you’re using MicroKernelTrait or registerContainerConfiguration() if not - it makes no difference) - I create a public alias to the real private service.
Here is an example - https://github.com/symfony/...
Let me know what you think!
Cheers!
Greetings, Ryan! Thank you for a very useful piece of advice concerning testing private services from inside the bundle test suite. As an idea, it's a great way to get access to a private service object.
So I ended up with the following code:
class KnpULoremIpsumTestingKernel extends Kernel
{
/**
* Returns a string with a service id for testing
* referencing the service with the id from the first argument.
*/
public static function getTestServiceId(string $c_service_id, string $c_prefix = 'public', string $c_delimiter = '.'): string
{
return $c_prefix . $c_delimiter . $c_service_id;
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
// ...
$c_test_service_id = self::getTestServiceId(FunctionalTest::MAIN_BUNDLE_SERVICE);
// no Alias object since no clear benefit
$container_builder->setAlias($c_test_service_id, FunctionalTest::MAIN_BUNDLE_SERVICE)
->setPublic(true);
}
}
class FunctionalTest extends WebTestCase
{
const MAIN_BUNDLE_SERVICE = 'knpu_lorem_ipsum.knpu_ipsum';
public function testServiceWiring()
{
$kernel = new KnpULoremIpsumTestingKernel();
$kernel->boot();
$container = $kernel->getContainer();
$c_test_service_id = KnpULoremIpsumTestingKernel::getTestServiceId(FunctionalTest::MAIN_BUNDLE_SERVICE);
if ($container->has($c_test_service_id)) {
$ipsum = $container->get($c_test_service_id);
} else {
throw new \LogicException(sprintf('Cannot get service "%s" or its alias "%s" from the container. ', self::MAIN_BUNDLE_SERVICE, $c_test_service_id));
}
$this->assertInstanceOf(KnpUIpsum::class, $ipsum);
// ...
So, thank you since now I know 2 ways of how to test private services in the bundle :)
On the side note. You may as well stop reading here.
At the same time, I was quite surprised looking at the code in ContainerBuilder::setAlias()
method. It has non-intuitive pieces here and there.
First, it doesn't type-hint its arguments ($alias
and $id
). While I speculate it has something to do with backward compatibility, the method doesn't care much if the first argument ($alias
) is of class Symfony\Component\DependencyInjection\Alias
or just an ordinary string. It type-casts the variable as a (string)
in either case and doesn't take advantage of an object. In my view, with object in the $alias
variable, there should be no need to set "public" property outside the method's scope since the relevant value can be accessed from Alias
object itself.
The whole Alias class looks strange to my taste. Now, it looks more like an AliasId
class. I'd expect the Alias
class to store both an alias id and the referenced service id. If this is the case, the ContainerBuilder::setAlias()
signature would be so much clearer:
public function setAlias(Alias $alias)
{
$aliasId = $alias->getId();
$serviceId = $alias->getReferencedServiceId();
$public = $alias->getPublic();
//...
}
So, by creating a new Alias
object with $public
property set to "true" by default, we still have to change the public status of the aliased service in our custom Kernel class with setPublic(true)
. Non-intuitive...
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"doctrine/annotations": "^1.8", // v1.8.0
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knpuniversity/lorem-ipsum-bundle": "*@dev", // dev-master
"nexylan/slack-bundle": "^2.0,<2.2", // v2.0.1
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.1.6
"symfony/asset": "^4.0", // v4.0.6
"symfony/console": "^4.0", // v4.0.6
"symfony/flex": "^1.0", // v1.18.7
"symfony/framework-bundle": "^4.0", // v4.0.6
"symfony/lts": "^4@dev", // dev-master
"symfony/twig-bundle": "^4.0", // v4.0.6
"symfony/web-server-bundle": "^4.0", // v4.0.6
"symfony/yaml": "^4.0", // v4.0.6
"weaverryan_test/lorem-ipsum-bundle": "^1.0" // v1.0.0
},
"require-dev": {
"easycorp/easy-log-handler": "^1.0.2", // v1.0.4
"sensiolabs/security-checker": "^4.1", // v4.1.8
"symfony/debug-bundle": "^3.3|^4.0", // v4.0.6
"symfony/dotenv": "^4.0", // v4.0.6
"symfony/maker-bundle": "^1.0", // v1.1.1
"symfony/monolog-bundle": "^3.0", // v3.2.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.3
"symfony/stopwatch": "^3.3|^4.0", // v4.0.6
"symfony/var-dumper": "^3.3|^4.0", // v4.0.6
"symfony/web-profiler-bundle": "^3.3|^4.0" // v4.0.6
}
}