Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Service Integration 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.

Start your All-Access Pass
Buy just this tutorial for $8.00

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

Login Subscribe

Thanks 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.

Bootstrapping the Integration Test

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?

Dependencies: symfony/framework-bunde?

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.

Adding our Dependencies

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.

Leave a comment!

41
Login or Register to join the conversation
Default user avatar
Default user avatar Saif Ali Ansari | posted 1 year ago
$ vendor/bin/simple-phpunit
PHPUnit 8.5.21 by Sebastian Bergmann and contributors.

Testing Test suite
E... 4 / 4 (100%)

Time: 500 ms, Memory: 8.00 MB

There was 1 error:

1) KnpU\LoremIpsumBundle\Tests\FunctionalTest::testServiceWiring
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: The service "knpu_lorem_ipsum.knpu_ipsum" has a dependency on a non-existent service "knpu_lorem_ipsum.knup_word_provider".

C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass.php:86
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\AbstractRecursivePass.php:82
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass.php:49
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\AbstractRecursivePass.php:91
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass.php:49
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\AbstractRecursivePass.php:82
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass.php:49
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\AbstractRecursivePass.php:46
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass.php:40
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\Compiler\Compiler.php:94
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\dependency-injection\ContainerBuilder.php:762
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\http-kernel\Kernel.php:596
C:\Users\SAIF\Desktop\LoremIpsumBundle\vendor\symfony\http-kernel\Kernel.php:136
C:\Users\SAIF\Desktop\LoremIpsumBundle\tests\FunctionalTest.php:16

ERRORS!
Tests: 4, Assertions: 658, Errors: 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
Reply

Hey Saif!

Could you elaborate on this? What question do you have? I only see the dumped PHPUnit output.

Cheers!

Reply
Default user avatar
Default user avatar Saif Ali Ansari | Victor | posted 1 year ago

the error is resolve

thank Victor Bocharsky for replying me.

Reply

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!

Reply
Default user avatar
Default user avatar Saif Ali Ansari | Victor | posted 1 year ago | edited

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

Reply

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!

Reply
Saverio S. Avatar
Saverio S. Avatar Saverio S. | posted 2 years ago

Hi, really geat tutorial. When I run simple-phpunit a recive a message "no tests executed".
Any Idea.

thks.
Xaver

Reply

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!

-1 Reply
Saverio S. Avatar

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.

Reply

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!

Reply
Ecm2 Avatar
Ecm2 Avatar Ecm2 | posted 2 years ago | edited

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

Reply

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!

Reply
erop Avatar
erop Avatar erop | posted 3 years ago | edited

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?

Reply
sadikoff Avatar sadikoff | SFCASTS | erop | posted 3 years ago | edited

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!

Reply
erop Avatar
erop Avatar erop | sadikoff | posted 3 years ago | edited

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?

Reply
sadikoff Avatar sadikoff | SFCASTS | erop | posted 3 years ago | edited

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 =)

Reply
Renaud G. Avatar
Renaud G. Avatar Renaud G. | posted 3 years ago | edited

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 ^^

Reply

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!

Reply

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?

Reply

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!

Reply

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.

Reply

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!

1 Reply

Thank you Victor for the response. I will again try the course from scratch after finishing other course from SC.

Reply

Hey Dushyant,

OK, let us know if you still have some misunderstanding and we will try to help!

Cheers!

Reply

Thank you Victor. By the way enjoying the tutorials. SymfonyCasts rocks.

Reply

Hey Dushyant,

Ah, thanks for the kind words about our screencasts! It always pushes us forward to make new awesome screencasts ;)

Cheers!

Reply
Edward B. Avatar

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.

1 Reply

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!

Reply
Maik T. Avatar
Maik T. Avatar Maik T. | posted 3 years ago | edited

Just a notice:

assertInternalType() is set as deprecated and shows warnings.

but you can simply use assertTrue(is_string($value))

Reply

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!

Reply
David L. Avatar
David L. Avatar David L. | posted 4 years ago

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?

Reply

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!

Reply
David L. Avatar
David L. Avatar David L. | Victor | posted 4 years ago | edited

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)
    {

    }
}
Reply

Hey David L. ,

I'm glad you was able to solve this! And thank you for sharing your solution with others.

Cheers!

Reply
Carlos Avatar

Hey, what's the correct way to test those services which are not public?

Reply

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!

Reply
Volodymyr T. Avatar
Volodymyr T. Avatar Volodymyr T. | Victor | posted 2 years ago

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.

Reply

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!

Reply
Volodymyr T. Avatar
Volodymyr T. Avatar Volodymyr T. | Victor | posted 2 years ago | edited

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&quot;&gt;repo with the screecast code is at https://github.com/voltel/knpu-lorem-ipsum-bundle&lt;/a&gt;, 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);
       // ...
Reply

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!

Reply
Volodymyr T. Avatar
Volodymyr T. Avatar Volodymyr T. | weaverryan | posted 2 years ago | edited

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...

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built using Symfony 4, but most of the concepts apply fine to Symfony 5!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice