Last active
May 22, 2024 11:23
-
-
Save lyrixx/0adb8fd414451596557871d2d9af5695 to your computer and use it in GitHub Desktop.
Test applications services can boot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
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 tosrc
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 menas 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 it 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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@lyrixx closing
}
for the test class is missing in the gist 😊.