kununu/testing-bundle
Kununu Testing Bundle provides reusable helpers and utilities for testing Symfony applications, simplifying test setup, mocking, and assertions so teams can write cleaner, more maintainable PHPUnit tests.
Install and enable (test env only).
composer require --dev kununu/testing-bundle
// config/bundles.php
return [
// ...
Kununu\TestingBundle\KununuTestingBundle::class => ['test' => true],
];
If your app has no config/bundles.php (older Symfony) or you prefer explicit control, register it in config/packages/test/kununu_testing.yaml-adjacent wiring and confirm the bundle is not loaded in prod/dev.
Where to look first (in this order):
README.md and the docs/ tree — as of v23.1.0 the project ships a multi-file documentation structure (README plus three companion documents) instead of a single monolithic README. Start at the README for install/enable, then follow the links for configuration and the test-case API. Confirm the actual filenames against the repo — the split is recent and the README is still the entry point.src/ — find the base TestCase/trait classes and the fixture-loading service. This is what you will actually extend.src/DependencyInjection/Configuration.php — the config tree tells you what is tunable (fixture paths, connection/entity manager, purge behaviour).src/Resources/config/*.xml|yaml — service IDs and their defaults; useful when you need to override or decorate.tests/ — the fastest way to copy a working example.The class names and service IDs below follow the conventions implied by the package description. Verify the exact FQCNs against src/ before committing to them — the surface area is small and lightly documented.
First use case: replace hand-rolled fixture loading in a functional test.
Before, most Symfony codebases end up with a setUp() that boots the kernel, calls doctrine:fixtures:load, and hopes for the best. The bundle's value is collapsing that into a base test case plus a loadFixtures() helper with sane defaults for the test environment.
use Kununu\TestingBundle\Test\FixturesTestCase; // confirm FQCN
final class OrderControllerTest extends FixturesTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->loadFixtures([OrderFixtures::class]);
}
public function testIndexListsOrders(): void
{
$client = static::createClient();
$client->request('GET', '/orders');
self::assertResponseIsSuccessful();
}
}
That is the 80% case: boot kernel → load a fixture set → assert. Everything else is variation on it.
Load the minimum data the test needs, in setUp(). Class-level loading looks cheaper but creates coupling: a test that only reads one order still pays for the full graph, and the first assertion failure tends to be a fixture error rather than a product bug.
protected function setUp(): void
{
parent::setUp();
$this->loadFixtures([UserFixtures::class, OrderFixtures::class]);
}
Keep fixture classes small and single-purpose (UserFixtures, ProductFixtures, OrderFixtures) and let Doctrine's getDependencies() express ordering. The bundle's loader will resolve dependencies for you; relying on array order in loadFixtures() works until someone reorders the arguments.
final class OrderFixtures extends Fixture
{
public function getDependencies(): array
{
return [UserFixtures::class, ProductFixtures::class];
}
}
Fixture references are the cleanest bridge between data setup and assertions. Store the object ID or reference name on the fixture and read it in the test — do not SELECT the record back to find its ID.
// in the test
$order = $this->getReference('order.pending', Order::class);
$client->request('GET', sprintf('/orders/%s', $order->getId()));
Note the second argument: without it, newer Doctrine fixture versions warn about untyped references.
Two viable strategies, pick one and be consistent across the suite:
sync transport, explicit flush() in a command, nested transactions in DDL).purgeDatabase()-style method on the base case.If the bundle offers a purge mode in config, enable it in CI and keep transactions for local fast feedback only if the two agree. Divergence here is the single most common source of "passes locally, fails in CI".
Kernel booting is the expensive part. Reuse the static::getContainer() / static::createClient() accessors from the bundle's base class rather than instantiating kernels yourself — the bundle is responsible for setting APP_ENV=test and keeping a single booted kernel per test class where that is safe.
For table-driven tests, build a small fixture that accepts constructor arguments and load it with per-case data:
/**
* @dataProvider invalidPayloadProvider
*/
public function testRejectsInvalidPayload(array $payload): void
{
$this->loadFixtures([new OrderFixtures(payload: $payload)]);
// ...
}
This keeps the fixture definition in one place while letting the test own the variation.
The bundle is a loader + base-class kit, not a full fixture DSL. If the codebase already uses Foundry or Alice, keep those for entity construction and let this bundle own when fixtures load and which connection they load into. Don't run two competing fixture systems against the same tables in one test.
Introduce the bundle by extending its base case in new tests only. Retro-fitting an existing WebTestCase hierarchy means auditing every setUp() for double-loading and stale kernel state — a mechanical but large diff. New-test-only adoption gets you the fixture helper immediately and lets you migrate opportunistically.
- run: bin/console doctrine:database:create --env=test --if-not-exists
- run: bin/console doctrine:schema:create --env=test
- run: bin/phpunit
Create the schema once per pipeline; let the bundle handle data per test. Also assert in CI that the bundle is absent from the prod container (bin/console debug:container --env=prod | grep -i kununu should be empty) — cheap insurance against the ['test' => true] flag being "fixed" by someone.
Environment leakage is the main risk. A testing bundle that boots kernels is only safe if it is registered for test alone. Check config/bundles.php after any merge that touches it, and prefer conditional registration over blanket ['all' => true].
Entity manager and connection must match production wiring. In multi-EM setups (read/write splits, bounded contexts), the fixture loader defaults to the doctrine manager. If you assert against a different connection than the one fixtures loaded into, tests fail with confusing "row not found" errors. Pass the manager name explicitly wherever the API allows it.
Foreign keys and fixture order. Truncation via DELETE respects FK constraints and will fail on cyclic or unordered tables; TRUNCATE ... CASCADE / SET FOREIGN_KEY_CHECKS=0 does not. Know which one the bundle's purge uses for your platform (MySQL vs PostgreSQL behave differently), and mirror it in your local DB.
Kernel reuse hides state. Symfony reboots the kernel between tests only when it detects the client was shut down. Static properties, in-memory caches, and container singletons survive across tests in the same class. If a test passes alone and fails in a suite, suspect this before suspecting the bundle.
Cache directories. With APP_DEBUG=0 in the test env, container changes are not picked up automatically. If a new service or config key "doesn't exist", clear var/cache/test before debugging anything else.
Debugging recipe. Run a single test with --filter and --stop-on-failure, then inspect the SQL: enable doctrine.dbal.logging in the test config or tail var/log/test.log. Nine times out of ten the fixture simply didn't load — assert that directly with a count query rather than inferring it from a 500.
Extension points. For anything the bundle doesn't cover, decorate rather than fork: alias the fixture loader service to your own implementation, or add a project-level base test case that extends the bundle's and adds your own helpers (actingAs(), tenant setup, clock freezing). Keep that subclass as the thing your suite extends — it gives you one place to absorb a breaking change in the package.
Maintenance risk is real. Low star count, no dependents, single vendor. Treat it as a convenience layer you could replace in an afternoon: keep your project-specific helpers in your own base class, avoid spreading bundle-specific APIs across hundreds of tests, and pin the version in composer.json ("kununu/testing-bundle": "~23.1" or an exact tag such as v23.1.0). Note the package is on a high major version and bumps majors routinely — a "23.1.0" minor is maintenance, but a "24.0.0" may not be, so pin loosely within the major and read the changelog before crossing one. If the package stalls, you want the migration to be "change the parent class", not "rewrite the suite".
Dev-tooling bumps ride along with releases. This project bumps kununu/code-tools and GitHub Actions in the same releases as runtime changes. Those can alter your CS/static-analysis behaviour if you share the tooling, without any change to the bundle itself — check whether a release is code or tooling before scheduling an upgrade. v23.1.0 was tooling/docs only (four-file docs structure, code-tools v5.1, Actions bumps): a safe drop-in from v23.0.1.
It is not a replacement for a real fixture library. The description promises "helper classes and fixtures loading" — that is orchestration, not data modelling. Expect to keep Doctrine Data Fixtures (or Foundry) underneath, and budget accordingly.
How can I help you explore Laravel packages today?