Skip to content

Instantly share code, notes, and snippets.

@lyrixx
Last active February 6, 2025 08:01
Show Gist options
  • Save lyrixx/0adb8fd414451596557871d2d9af5695 to your computer and use it in GitHub Desktop.
Save lyrixx/0adb8fd414451596557871d2d9af5695 to your computer and use it in GitHub Desktop.
Test applications services can boot
<?php
namespace Tests\Integration;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
class ContainerTest extends KernelTestCase
{
private const FILTER_LIST = [
// some services can exist only in dev or prod (thus not in test env)
// or some services are behind some features flags
// or some services are static (thus they are not real service)
];
public function testContainer()
{
static::bootKernel(['debug' => true]);
$projectDir = static::getContainer()->getParameter('kernel.project_dir');
$container = static::getContainer();
$builder = new ContainerBuilder();
$loader = new XmlFileLoader($builder, new FileLocator());
$loader->load($container->getParameter('debug.container.dump'));
$count = 0;
foreach ($builder->getDefinitions() as $id => $service) {
if ($this->isSkipped($id, $service, $builder, $projectDir)) {
continue;
}
$container->get($id);
++$count;
}
$this->addToAssertionCount($count);
}
private function isSkipped(string $id, Definition $service, ContainerBuilder $builder, string $projectDir): bool
{
if (str_starts_with($id, '.instanceof.') || str_starts_with($id, '.abstract.') || str_starts_with($id, '.errored.')) {
return true; // Symfony internal stuff
}
if ($service->isAbstract()) {
return true; // Symfony internal stuff
}
$class = $service->getClass();
if (!$class) {
return true; // kernel, or alias, or abstract
}
if (\in_array($class, self::FILTER_LIST)) {
return true;
}
$rc = $builder->getReflectionClass($class, false);
if (!$rc) {
return true;
}
$filename = $rc->getFileName();
if (!str_starts_with($filename, "{$projectDir}/src")) {
return true; // service class not in tests/Integration
}
if ($rc->isAbstract()) {
return true;
}
return false;
}
}
Copy link

ghost commented Aug 12, 2022

Thank you. I'm testing this script right now. The first problem I see is that it throws on unused services i.e. the services that are auto registered via resource: '../src/*'. AFAIR such services are removed during container compilation because they are not dependencies of any other service (exception classes, Models, ValueObjects and so on). I know those classes can be excluded via exclude option but this looks like some additional work that we don't do on a daily basis, just to satisfy your script. WDYT?

Second problem: private services will throw here. Do you assume that all the services are public in the test env?
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: The "App\Security\MyJwtAuthenticator" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.

@lyrixx
Copy link
Author

lyrixx commented Aug 12, 2022

Yes! indeed. But it's better to exclude from the DIC theses service (less CPU/time consumed for nothing)

I use this pattern all the time:

    AppBundle\Crawler\:
        resource: '../../../src/Crawler/*'
        exclude:
            - '../../../src/Crawler/**/Exception/*'
            - '../../../src/Crawler/**/Model/*'
            - '../../../src/Crawler/Messenger/Message/*'

About the 2nd problem, I do have lot of private service (almost all of them) and it works nicely.

Symfony, in test env, have a special container where all service are kinda public

Copy link

ghost commented Sep 27, 2022

if (!str_starts_with($filename, "{$projectDir}/src")) {
    return true; // service class not in tests/Integration
}

Why do we check only classes from src/? This is common that services from vendor packages are configured by the application via a bundle config or custom container extension or compiler pass. We want to check instantiating those as well (to verify we have configured them correctly).

@lyrixx
Copy link
Author

lyrixx commented Sep 28, 2022

@javaDeveloperKid you're right. I coded that only for my use case. But feel free to adapt it!

@ruudk
Copy link

ruudk commented Aug 7, 2023

Thanks for sharing this.

You can use this to automatically find the XML file:

$xml = file_get_contents($container->getParameter('debug.container.dump'));

Another improvement would be to use the XmlFileLoader so that you don't have to manually parse and work with the XML structure:

$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator());
$loader->load(static::getContainer()->getParameter('debug.container.dump'));

foreach ($container->getDefinitions() as $service) {
    // $service = Definition now...
}

@lyrixx
Copy link
Author

lyrixx commented Aug 7, 2023

Very good idea. I'll update the gist asap

@lyrixx
Copy link
Author

lyrixx commented Aug 24, 2023

Gist updated ๐ŸŽ‰ Thanks again

@ruudk
Copy link

ruudk commented Aug 31, 2023

In our project, we have a lot of service locators. I think the test should take these into account and unpack every ServiceLocator that it finds.

@ruudk
Copy link

ruudk commented Aug 31, 2023

Another optimization that can be made: report all failures, instead of only the first one.

@Wirone
Copy link

Wirone commented May 21, 2024

@lyrixx closing } for the test class is missing in the gist ๐Ÿ˜Š.

@Wirone
Copy link

Wirone commented May 22, 2024

FYI, I ended up with something like this as a PoC:

class ContainerTest extends KernelTestCase
{
    private const FILTER_LIST = [
        // some services can exist only in dev or prod (thus not in test env)
        // or some services are behind some features flags
        // or some services are static (thus they are not real service)
    ];

    public function testContainer(): void
    {
        static::bootKernel(['debug' => true]);
        $projectDir = static::getContainer()->getParameter('kernel.project_dir');
        $container = static::getContainer();

        $this->bootstrapContainer($container);

        $builder = new ContainerBuilder();
        $loader = new XmlFileLoader($builder, new FileLocator());
        $loader->load($container->getParameter('debug.container.dump'));

        $invalidServices = [];
        $count = 0;

        foreach ($builder->getDefinitions() as $id => $service) {
            if ($this->isSkipped($id, $service, $builder, $projectDir)) {
                continue;
            }

            try {
                $container->get($id);
            } catch (\Throwable $e) {
                $invalidServices[] = sprintf('[%s] %s', $id, $e->getMessage());
                continue;
            }

            ++$count;
        }

        $this->addToAssertionCount($count);

        if (count($invalidServices) > 0) {
            throw new \RuntimeException(sprintf(
                "Invalid services found: \n  - %s",
                implode("\n\n  - ", $invalidServices)
            ));
        }
    }

    private function isSkipped(string $id, Definition $service, ContainerBuilder $builder, string $projectDir): bool
    {
        if (str_starts_with($id, '.instanceof.')
            || str_starts_with($id, '.abstract.')
            || str_starts_with($id, '.errored.')
            || $service->isAbstract() // Incomplete definitions that can't be instantiated
            || $service->isSynthetic() // Synthetic services don't actually have a definition, these are set in runtime
            || $service->isLazy() // @TODO support lazy services
        ) {
            return true; // Symfony internal stuff
        }

        $class = $service->getClass();
        if (!$class) {
            return true; // kernel, or alias, or abstract
        }

        if (\in_array($class, self::FILTER_LIST)) {
            return true;
        }

        $rc = $builder->getReflectionClass($class, false);
        if (!$rc) {
            return true;
        }

        $filename = $rc->getFileName();
        if (false === $filename || str_starts_with($filename, "{$projectDir}/vendor/symfony")) {
            return true;
        }

        if ($rc->isAbstract()) {
            return true;
        }

        return false;
    }

    /**
     * Do all the stuff required for bootstrapping services/factories, that are used when services are being built
     */
    private function bootstrapContainer(ContainerInterface $container): void {
    }
}

The differences from gist's version:

  • iterate over all service, collect all the errors and print them at once at the end
  • ignore synthetic services (these are meant to be set in runtime, no need to check them)
  • ignore lazy services (it's something to tackle, I had some problems and did not want to spend much time on it)
  • ignore Symfony services registered from vendor/symfony, instead of narrowing to src only (our app has several dirs with production code, I did not want to list them all or to prepare complex logic)
  • skip if service reflection returned false

Anyway, thanks for sharing, it was good to make some kind of experiment. What I found more than lint:container is that we rely on EventDispatcher, not on EventDispatcherInterface, so when traceable event dispatcher is injected in test environment, there's TypeError, which means we should fix the signatures and use the interface as a contract.

PS. in our case bootstrapContainer() has logic, I just removed it as it's crafted for our code. But if there are any things that need to be pre-defined before services are created (like $_SERVER['HTTP_CLIENT_IP'] for example), you also need to do it before iterating over container.

@nikophil
Copy link

nikophil commented Feb 5, 2025

hey @lyrixx

thanks for this snippet.

I'm trying to implement this in one huge project, and I'm facing some memory limit problems. I tried to run with 'debug' => false, or to shutdown kernel, but still have a huge memory usage with this test. Do you have some hints about this? thanks!

@lyrixx
Copy link
Author

lyrixx commented Feb 5, 2025

@nikophil

On project I'm working ATM:

  • 6557 services in total
  • OK (1 test, 1008 assertions)
    Time: 00:01.455, Memory: 119.00 MB
    
    so 1008 services tested

So it looks like it does not consume too much memory

Maybe you have a memory leak somewhere...

@nikophil
Copy link

nikophil commented Feb 5, 2025

we have more than 20.000 services and 4000 services tested, memory consumption ends at ~600 Mo just for this test ๐Ÿ˜ž

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment