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!

  • 2020-06-12 Vladimir Sadicov

    Hey Egor Ushakov
    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 =)

  • 2020-06-05 Egor Ushakov

    Hey Vladimir Sadicov !
    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 concrete 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?

  • 2020-06-05 Vladimir Sadicov

    Hey Egor Ushakov

    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!

  • 2020-06-03 Egor Ushakov

    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?

    I understand the cause of an error. But what approach should be applied for testing bundles in this case?

  • 2020-05-12 weaverryan

    Hey Brian!

    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!

  • 2020-05-08 Brian

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

    Is there a best practice or a consensus? Thanks a lot!

    Edit3: finally got it, posting my code in case it helps others:


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

  • 2020-01-20 weaverryan

    Welcome to Symfony Ed Barnard! 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!

  • 2020-01-17 Ed Barnard

    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.

  • 2020-01-17 Victor Bocharsky

    Hey Dushyant,

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

    Cheers!

  • 2020-01-17 Dushyant Joshi

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

  • 2020-01-17 Victor Bocharsky

    Hey Dushyant,

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

    Cheers!

  • 2020-01-16 Dushyant Joshi

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

  • 2020-01-16 Victor Bocharsky

    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!

  • 2020-01-13 Dushyant Joshi

    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.

  • 2020-01-10 Victor Bocharsky

    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!

  • 2020-01-10 Dushyant Joshi

    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?

  • 2019-12-10 Victor Bocharsky

    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!

  • 2019-12-05 Maik Tizziani

    Just a notice:

    assertInternalType() is set as deprecated and shows warnings.

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

  • 2019-07-17 Victor Bocharsky

    Hey dlegatt ,

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

    Cheers!

  • 2019-07-16 dlegatt

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

    }
    }
  • 2019-07-16 Victor Bocharsky

    Hey dlegatt ,

    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!

  • 2019-07-10 dlegatt

    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?

  • 2019-02-06 Victor Bocharsky

    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!

  • 2019-02-06 Carlos Eduardo

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