mekras/symfony-bundle-testing
Testing utilities for Symfony bundles: helpers for booting test kernels, configuring services, and writing functional and integration tests around your bundle code. Simplifies PHPUnit setup so you can focus on assertions.
This package gives you a fluent way to compile a Symfony service container in tests and assert on its wiring. Day-to-day, you'll use it to prove that your bundle/extension config actually produces the services you expect — without booting a full kernel.
Minimal steps:
require-dev dependency.BaseSymfonyIntegrationTestCase (or whichever base test case your suite already uses) in your integration test.TestContainerBuilder — it is the new entry point for everything: extensions, bundles, locatability, visibility, and assertions.use Acme\Testing\BaseSymfonyIntegrationTestCase;
final class ServiceWiringTest extends BaseSymfonyIntegrationTestCase
{
public function test_service_is_registered(): void
{
$this->createContainerFromFile(__DIR__.'/Fixtures/services.yaml')
->expectDefinitionsExists(MyService::class);
}
}
The first real use case is almost always a regression guard: you changed an extension's load() method or a bundle's services.yaml, and you want a test that fails loudly when a definition disappears or stops being public.
Build once per test, assert fluently. createContainer() / createContainerFromFile() now return a TestContainerBuilder, so the natural style is a single chain:
$builder = $this->createContainerFromFile(__DIR__.'/Fixtures/config.yaml');
$builder
->registerBundle(AcmeBundle::class)
->loadExtension('acme', ['api_key' => 'test', 'debug' => true])
->makePublic()
->expectDefinitionsExists(Acme\Client::class);
Configuration-matrix testing. Put config arrays in a data provider and build a fresh container per case. The builder is cheap enough to create per test, and isolating it avoids leaking compiled state between cases.
Locator-based retrieval for behaviour tests. When you want to call the service rather than just assert it exists, mark it locatable and pull it out:
$service = $this->createContainerFromFile(__DIR__.'/Fixtures/config.yaml')
->loadExtension('acme', ['transport' => 'null'])
->makeLocatable(Acme\Mailer::class)
->locate(Acme\Mailer::class);
self::assertInstanceOf(Acme\Mailer::class, $service);
Migrate legacy tests incrementally. Every deprecated base-class method has a builder equivalent, so old tests can be rewritten mechanically without changing what they assert:
Deprecated on BaseSymfonyIntegrationTestCase |
Replacement on TestContainerBuilder |
|---|---|
loadExtension() |
loadExtension() |
registerBundle() |
registerBundle() |
expectServiceExists() |
expectDefinitionsExists() |
addToLocator() |
makeLocatable() |
getFromLocator() |
locate() |
createContainer(..., $public: true) |
createContainer(...)->makePublic() |
Integration tip: treat the builder as the seam for your own helpers. A small makeContainer(array $config = []) factory in a base test case keeps the fluent chain out of individual tests and gives you one place to update when the API moves again.
Extension points: loadExtension() is the hook for exercising your Extension::load()/prepend() logic in isolation, and registerBundle() lets you verify bundle-level prepend() behaviour without a kernel. These two cover most of the value in a bundle's test suite.
createContainer() and createContainerFromFile() now return TestContainerBuilder, not a container. Any test doing $this->createContainer()->get(Foo::class) will break. Replace with ->locate(Foo::class) (after makeLocatable()) or assert via expectDefinitionsExists() instead of resolving.$public argument is deprecated, not removed. createContainer(public: true) still works today but emits deprecations. Switch to ->makePublic() now, while the test suite is already green, rather than during a later upgrade.expectServiceExists() → expectDefinitionsExists() is a rename with a nuance. The new name says definitions, which is what you're actually asserting. Aliases, synthetic services, and services produced by factories may not show up the way a "service exists" assertion implied — check those cases explicitly.makePublic()/makeLocatable() must come after the definition exists. If a lookup returns nothing, check the ordering before assuming a wiring bug.locate() only works for services you've passed through makeLocatable(). A missing entry is usually a forgotten makeLocatable(), not a broken container.$builder->locate('service_container') or the kernel's debug:container equivalent) and diff against expectations. Definition IDs, not class names, are the usual source of surprise.TestContainerBuilder for container introspection in application code.How can I help you explore Laravel packages today?