macpaw/behat-doctrine-fixtures
Behat extension for Doctrine fixtures that simplifies loading and purging test data in Behat scenarios. Integrates Doctrine ORM fixtures with Behat contexts so acceptance tests can seed databases reliably and keep suites isolated.
Install as a dev dependency. The package is only useful in test code, so keep it out of production autoload paths:
composer require --dev macpaw/behat-doctrine-fixtures
You will also need a working Doctrine setup (doctrine/orm, doctrine/data-fixtures) and Behat. If you are on Symfony, the smoothest path is to have behat/symfony2-extension boot the kernel so the EntityManager is available as a service.
Check your ORM major before anything else. v2.2.0 ships the Doctrine ORM 3 upgrade (the PR is titled "Release 3.0"). If you are still on ORM 2, stay on the 2.1.x line; if you are on ORM 3, you are on the new one. Read composer.json in the tag you install and confirm the doctrine/orm constraint, because the release notes and the PR title disagree about the version number.
Register the context in behat.yml. The package is a Behat context, not a bundle. Add it alongside your application context:
default:
suites:
default:
contexts:
- App\Tests\Behat\FeatureContext
- MacPaw\BehatDoctrineFixtures\Context\DoctrineFixturesContext
If your project is not Symfony-based, you may need to pass the EntityManager explicitly through the context constructor arguments; check src/Context for the exact service name expected by your version.
Look here first when onboarding:
README.md — the step definitions and the expected behat.yml wiring.src/Context/*Context.php — every Gherkin step the package exposes lives here. This is the real API surface.src/Loader/ (or equivalent loader namespace) — the abstraction for how fixtures are discovered and executed.composer.json — supported Behat major, Doctrine ORM 2 vs 3, doctrine/data-fixtures version, and PHP constraints. With low adoption, this is your compatibility source of truth.First use case: per-scenario fixture loading. The typical day-one pattern is:
@fixtures=users
Scenario: A user can log in
Given the database contains the "users" fixtures
When I log in as "[email protected]"
Then I should see "Welcome, Alice"
Back that with a @BeforeScenario hook that purges and loads the tagged fixture set. Start with one fixture file, verify it loads in isolation, then expand to tagged sets.
Use hooks for lifecycle, tags for selection. Keep database reset and fixture loading in @BeforeScenario hooks, and use tags such as @fixtures=users or @fixtures=reference to decide what to load. This keeps feature files readable and avoids hardcoding fixture paths inside step text.
/** @BeforeScenario */
public function loadFixtures(BeforeScenarioScope $scope): void
{
$tags = $scope->getScenario()->getTags();
// decide whether to purge, then load the requested fixture set
}
Place the reset/fixture context before your feature context in behat.yml if your own hooks depend on fixtures already being present. Behat executes hooks in context registration order.
Separate reference data from scenario data. Load slow, shared data (countries, roles, permissions) once in a @BeforeSuite or a tagged background, and load per-scenario records in @BeforeScenario. Purging and reloading everything for every scenario is the most common cause of slow Behat suites.
Prefer class-based fixtures for structure, YAML/Alice for volume. Use Doctrine Fixture classes when you need constructor logic, service calls, or complex relationships. Use YAML/Alice-style files when you want expressive, compact datasets. If the package supports an Alice integration, use it for nested object graphs; otherwise keep YAML flat and load in dependency order.
On ORM 3, class-based fixtures must use attributes. The annotation driver is gone. Anything in your fixtures or entities relying on @ORM\... docblocks will not be mapped. Port fixture-mapped entities to PHP attributes before upgrading, and check whether your loader or helper classes type-hint ClassMetadataInfo — that class was removed; use ClassMetadata instead.
Stop calling merge(). EntityManagerInterface::merge() was removed in ORM 3. Older fixture helpers that "attach" a detached entity with merge() will now fatal. Keep the loaded object references and reuse them, which is also the pattern that makes fixtures composable (below).
Compose fixtures through references, not IDs. Doctrine fixtures let one fixture reference another by object reference. Use that instead of hardcoded IDs; IDs differ between SQLite, MySQL, and Postgres, and between test runs. If you must assert on IDs, assert through the API/UI, not raw fixture values.
Isolate scenarios with transactions where possible. On ORM 3, EntityManager::transactional() is gone but wrapInTransaction() remains. For read-mostly tests, begin a transaction in @BeforeScenario and roll it back in @AfterScenario using the DBAL connection. Fall back to purge-and-load only for tests that exercise DDL, sequences, or code that commits. This is the single biggest performance lever in a Doctrine Behat suite.
Let Symfony inject the manager. With behat/symfony2-extension, the context can pull doctrine.orm.entity_manager from the container. Ensure the kernel is booted before fixtures load; a common pattern is to wrap loading in a kernel-boot check or rely on the extension's @BeforeScenario kernel hook.
Extension point: custom loader. If you outgrow the built-in loading behavior (e.g. multi-tenant databases, per-tenant schemas, or loading from a custom directory layout), implement the package's loader interface and register your implementation. This is cleaner than overriding step definitions in your feature context. Note that a custom purger or loader written against ORM 2/DBAL 3 will need DBAL 4 API updates — Connection::exec() and Statement::execute() are gone in favour of executeStatement() / executeQuery().
Debug a single scenario first. Run Behat with a name filter and pretty format:
vendor/bin/behat features/login.feature:12 --format=pretty --no-colors
If fixtures load but assertions fail, dump the database state after the hook rather than inside the step. The failure is usually in the fixture file, not in the step definition.
Low adoption means you own compatibility. With around seven stars and no dependents, expect to be the first to hit edge cases. Pin an exact version, read the release notes (and note that v2.2.0's notes describe a "Release 3.0" and an ORM 3 upgrade from a brand-new contributor), and check open issues before upgrading Behat, Doctrine, or PHP. Treat the package as a small internal utility you may need to fork.
The ORM 3 upgrade is the real breaking change. Beyond annotations and merge(), ORM 3 pulls in doctrine/persistence 4 and DBAL 4. If your app or another test dependency pins DBAL 3, Composer will refuse the upgrade — resolve that before assuming the package is at fault. getReference() now returns an uninitialized proxy without hitting the database, so assertions against a reference you never dereferenced will throw "Entity is not initialized"; fetch the real entity in the fixture when you need it in an assertion.
EntityManager staleness after failures. When a scenario throws inside a Doctrine operation, the EntityManager can be closed or left with a dirty Unit of Work. Reset or recreate it in @AfterScenario, and call clear() after bulk fixture loading so subsequent queries see fresh state. Reusing a closed EM produces confusing "EntityManager is closed" errors in later scenarios.
Purge order respects foreign keys. A naive purge fails on databases that enforce FKs. Use Doctrine's purger with the correct order, disable FK checks temporarily, or truncate tables in reverse dependency order. SQLite and MySQL behave differently here; test against the same engine you use in CI. If you supply a custom purger, re-verify it after the DBAL 4 bump — truncate/platform SQL helpers changed.
SQLite in-memory is a trap for fixture suites. If each scenario gets a fresh in-memory connection, schema creation and fixtures must both run per scenario, which is slow and often hides ordering bugs that appear on MySQL/Postgres. Prefer a file-backed test database or a shared connection with transactions for local runs.
Hook ordering and tag parsing are manual. The package will not magically know which fixtures a scenario needs. Parse tags defensively (tags can be @fixtures=users, @fixtures:users, or multiple tags) and make the loader idempotent. Loading the same fixture twice in one scenario is a common source of duplicate-key errors.
Autoload your test fixtures. Fixture classes under tests/ must be in autoload-dev. If the loader reports "class not found" for a fixture you can see on disk, run composer dump-autoload and check the namespace mapping — this is the most frequent setup mistake.
Watch the kernel lifecycle. With Symfony, the kernel may be rebooted between scenarios while the context holds a stale container reference. Fetch the EntityManager lazily (per scenario) rather than caching it in the constructor, especially if you use KernelBrowser or reboot the kernel manually.
Do not mix Background fixtures with hook fixtures. If a Background step loads data and your @BeforeScenario hook also purges, you will get nondeterministic ordering depending on Behat's hook execution. Choose one place to own database state — preferably the hook.
Use clear() and refresh() deliberately. After loading fixtures that create related entities, call $em->clear() before your step definitions query, then re-fetch. Assertions against stale managed entities are a frequent false-negative source.
Keep fixture files small and composable. One giant all.yml quickly becomes a merge-conflict hotspot. Split by aggregate (users.yml, orders.yml, products.yml) and load only what a scenario needs. This also makes it obvious which fixtures are safe to purge and which are reference data.
How can I help you explore Laravel packages today?