Open-source project
dmaicher/doctrine-test-bundle avatar
dmaicher/doctrine-test-bundle

doctrine-test-bundle: database tests that roll back instead of rebuilding

Symfony bundle to isolate your app's doctrine database tests and improve the test performance

1,197 stars61 forksPHPMIT

At a glance

What is it?
The dama/doctrine-test-bundle Symfony bundle wraps your configured Doctrine DBAL driver, holds one static connection per PHP process, and rolls back a transaction around every test case, so each test starts from the same database state without rebuilding the schema. It also ships a static PSR-6 cache for entity metadata and query cache.
Who is it for?
Adopt dama/doctrine-test-bundle if your Symfony test suite spends its time rebuilding schemas and reloading fixtures, and if your tests do not issue DDL that commits implicitly. Do not adopt it if a test needs to commit, since the whole model is a transaction that never commits.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • dmaicher/doctrine-test-bundle1,197 ★ · +4
Star history
Repository2026-10-202026-11-07
dmaicher/doctrine-test-bundle1.2k1.2k

4 Stars in the last 18 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

One connection, one transaction, one rollback

The bundle exists to make a Symfony application's test suite faster without giving up isolation. It provides a StaticDriver that wraps your originally configured Driver class, such as DBAL\Driver\PDOMysql\Driver, and keeps a database connection statically in the current PHP process. A PHPUnit extension then begins a transaction before every test case and rolls it back after the test finishes, for all configured DBAL connections.

The performance argument is the point. Because each test runs inside a transaction that is undone afterwards, there is no need to rebuild the schema, import a backup SQL dump, or re-insert fixtures before every test case. The database state that the first test sees is the state the hundredth test sees, provided the tests do not commit.

That proviso is explicit in the project documentation. As long as you avoid issuing DDL queries that might result in implicit transaction commits, such as ALTER TABLE and DROP TABLE, your tests stay isolated and all see the same database state. The documentation links to a wiki page on transactional DDL in PostgreSQL for the underlying reason.

The project is MIT licensed, written in PHP, and the current release is v8.7.0 from 2026-10-08, with the last push to the repository on the same day.

Installing the bundle and wiring the extension

The bundle is a dev dependency:

bash
composer require --dev dama/doctrine-test-bundle

If your project does not use Flex, you enable it by adding the class to bundles.php, restricted to the test environment:

php
<?php
// config/bundles.php

return [
    //...
    DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
    //...
];

Then the PHPUnit extension is registered in your PHPUnit XML configuration:

xml
<phpunit>
    ...
    <extensions>
        <bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
    </extensions>
</phpunit>

The documentation is careful about the PHPUnit version. You need phpunit/phpunit as a dev dependency, with versions 11, 12, and 13 supported with the built-in extension, and the bundle is also compatible with symfony/phpunit-bridge and its simple-phpunit script. If you go the bridge route, you may need to make sure the bridge requires the correct PHPUnit 10+ version, which the documentation says is set with the SYMFONY_PHPUNIT_VERSION environment variable.

There is one configuration step that catches people out, and it depends on your DBAL version. Starting from version 8 of the bundle, and only when using DBAL below 4, you need use_savepoints enabled on your Doctrine DBAL configuration for all relevant connections:

yaml
doctrine:
   dbal:
       connections:
           default:
               use_savepoints: true

Savepoints are what let a nested transaction inside your code be rolled back independently, which matters precisely because the test opens an outer transaction.

The static metadata and query cache

The second half of the bundle is a cache, and it addresses a cost that has nothing to do with the database. The Psr6StaticArrayCache is automatically configured as the metadata cache and the query cache for all entity managers. The project describes the effect as a dramatic improvement in speed and memory usage for the test suite.

The benefit is specific to tests that boot the kernel. Controller tests and container-aware tests each start a fresh container, and with a normal cache implementation the metadata for every entity class is parsed again on every boot. That parsing is pure CPU and it happens once per test, which is why a suite with a hundred controller tests spends a noticeable amount of time in class metadata rather than in the code under test. A static array cache keeps the parsed metadata in the PHP process, so the second and later kernel boots reuse it.

The reason it is scoped to tests is that a static cache in a long-running production process would never be invalidated. In a test process that lives for one suite run, staleness is not a problem, and the cache is discarded when the process ends. The query cache falls into the same category, since the DQL to SQL translation result is reusable as long as the mapping has not changed, and the mapping does not change during a suite.

Both cache flags are on by default in the bundle configuration:

yaml
dama_doctrine_test:
    enable_static_connection: true
    enable_static_meta_data_cache: true
    enable_static_query_cache: true

The first flag applies the static connection to all configured Doctrine DBAL connections, and it can also be given per connection if you need to be selective about which ones are held statically.

Skipping the rollback for specific tests

There are cases where the rollback model does not fit, and the bundle provides an attribute rather than making you restructure the test. The SkipDatabaseRollback attribute disables the rollback handling for a class, for an abstract parent class, or for a single test method:

php
#[SkipDatabaseRollback] // this will skip it for all tests in a class
public class MyTest extends \PHPUnit\Framework\TestCase {}

#[SkipDatabaseRollback] // also supported on (abstract) parent classes. So all tests in child classes will skip the rollback logic.
abstract class MyAbstractTest extends \PHPUnit\Framework\TestCase {}

#[SkipDatabaseRollback] // this will skip it for only one test method
public function MyTest() {}

The parent class case is the one worth noting, since a shared abstract base is where a lot of test code lives, and marking it once propagates to every child. The cost of using this attribute is that a skipped test leaves its data in the database, and since the connection is shared across the whole process, that data is visible to later tests. Any test marked this way has to clean up after itself.

The typical reason to reach for it is a test that needs to exercise commit behaviour, or one that runs DDL which would commit the surrounding transaction anyway, in which case the rollback would not restore the state regardless and the marker makes that explicit rather than accidental.

The warning about transactions nobody closed

This is the sharpest edge in the bundle and the documentation treats it as a warning section. Because all connections share one static connection and everything is rolled back at the end of the test, a transaction your code started but never committed or rolled back goes unnoticed: its changes stay visible for the rest of the test, even across kernel reboots between requests. Outside of tests those changes would be lost.

In other words, a test that opens a transaction and forgets to close it passes on the assertion that was written to check something else, and the pollution stays visible for the remainder of that test. The bundle catches this. The PHPUnit extension triggers a warning for such tests, so they fail when the test suite runs with failOnWarning="true".

The warning only works for tests with an after-hook like tearDown(), and both KernelTestCase and WebTestCase have one. A plain test case without that hook will not be checked, which is a boundary worth knowing.

If you want to turn the warning off, it is a parameter on the extension:

xml
<extensions>
    <bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension">
        <parameter name="warnAboutOpenTransactions" value="false"/>
    </bootstrap>
</extensions>

Turning it off is a deliberate choice. The warning is what makes an invisible bug visible, and disabling it means the unclosed transaction stays invisible again.

Using the same bundle with Behat

The bundle is not PHPUnit-only, and the Behat integration works the same way in principle, since a scenario gets a transaction and a rollback instead of a database rebuild. The configuration in behat.php uses the Behat config objects:

php
use Behat\Config\Config;
use Behat\Config\Extension;
use Behat\Config\Profile;
use DAMA\DoctrineTestBundle\Behat\ServiceContainer\DoctrineExtension;

return (new Config())
    ->withProfile(
        (new Profile('default'))
            // ...
            ->withExtension(new Extension(DoctrineExtension::class))
    );

And with a YAML config on Behat 3, the extension is listed under extensions:

yaml
default:
   # ...
   extensions:
       DAMA\DoctrineTestBundle\Behat\ServiceContainer\DoctrineExtension: ~

The limitation the documentation states is a hard one. This only works if the tests are executed in the same process as Behat, which means it cannot work when you use something like Selenium to call your application, since the application then runs in a browser process with its own database connection and the bundle's transaction has no effect on it. That constraint is the same one that applies to any transaction-per-test approach, and it is the reason the bundle cannot help with end-to-end tests driven through a real browser.

The connection key configuration is the other piece that matters for multi-connection setups. By default every configured DBAL connection gets its own driver connection managed in the current process, and connection_keys lets you assign several connections the same key so they share one underlying driver connection, which is what you want when two connections point at the same database. Since v8.1.0, for connections with read and write replicas the bundle uses the same underlying driver.

Editorial conclusion

Adopt dama/doctrine-test-bundle if your Symfony test suite spends its time rebuilding schemas and reloading fixtures, and if your tests do not issue DDL that commits implicitly. Do not adopt it if a test needs to commit, since the whole model is a transaction that never commits. Verify first that use_savepoints is enabled on every relevant connection when you are on DBAL below version 4, and that any test which starts a transaction it never closes is one you actually want to keep.

Frequently asked questions

How does dama/doctrine-test-bundle keep tests isolated?

It wraps your configured DBAL driver in a StaticDriver that keeps one connection in the current PHP process, then a PHPUnit extension begins a transaction before every test case and rolls it back afterwards for all configured connections. That removes the need to rebuild the schema or re-import fixtures between tests.

What is the use_savepoints requirement about?

Starting from version 8 of the bundle, and only when using DBAL below 4, you need use_savepoints enabled on the Doctrine DBAL configuration for all relevant connections. Savepoints let a nested transaction inside your code be rolled back independently, which matters because the test opens an outer transaction.

How do I skip the rollback for a single test?

Apply the SkipDatabaseRollback attribute to the test class, to an abstract parent class so all child tests inherit it, or to a single test method. A test marked this way leaves its changes in the database, so it has to clean up after itself.

What does the warning about transactions that were never closed mean?

Because all connections share one static connection and everything is rolled back at the end of the test, a transaction your code started but never closed goes unnoticed, and its changes stay visible for the rest of the test. The PHPUnit extension triggers a warning for such tests, so they fail under failOnWarning="true".

Can I use the bundle with Behat?

Yes, by registering the DoctrineExtension in the Behat configuration, either through the Behat config objects in behat.php or in the extensions section of a Behat 3 YAML file. It only works when the tests run in the same process as Behat, so it does not apply when Selenium drives the application.

Official sources

  1. dmaicher/doctrine-test-bundle on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dmaicher-doctrine-test-bundle.svg)](https://hysenlabs.com/projects/dmaicher-doctrine-test-bundle)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/dmaicher-doctrine-test-bundle.svg)](https://hysenlabs.com/projects/dmaicher-doctrine-test-bundle)

Was this useful?