dama / doctrine-test-bundle
Symfony bundle to isolate doctrine database tests and improve test performance
Package info
github.com/dmaicher/doctrine-test-bundle
Type:symfony-bundle
pkg:composer/dama/doctrine-test-bundle
Requires
- php: >= 8.2
- doctrine/dbal: ^3.3 || ^4.0
- doctrine/doctrine-bundle: ^2.11.0 || ^3.0
- psr/cache: ^2.0 || ^3.0
- symfony/cache: ^6.4 || ^7.3 || ^8.0
- symfony/framework-bundle: ^6.4 || ^7.3 || ^8.0
Requires (Dev)
- behat/behat: ^3.17 || ^4.0
- php-cs-fixer/shim: ^3.95
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5.41 || ^12.3.14 || ^13.0
- symfony/dotenv: ^6.4 || ^7.3 || ^8.0
- symfony/process: ^6.4 || ^7.3 || ^8.0
Suggests
None
Provides
None
Conflicts
- phpunit/phpunit: <11.0
Replaces
None
- dev-master / 8.x-dev
- v8.8.0
- v8.7.1
- v8.7.0
- v8.6.0
- v8.5.0
- v8.4.1
- v8.4.0
- v8.3.1
- v8.3.0
- v8.2.2
- v8.2.1
- v8.2.0
- v8.1.0
- v8.0.2
- v8.0.1
- v8.0.0
- v8.0.0-BETA8
- v8.0.0-BETA7
- v8.0.0-BETA6
- v8.0.0-BETA5
- v8.0.0-BETA4
- v8.0.0-BETA3
- v8.0.0-BETA2
- v8.0.0-BETA1
- 7.x-dev
- v7.3.0
- v7.2.1
- v7.2.0
- v7.1.1
- v7.1.0
- v7.0.0
- v6.7.5
- v6.7.4
- v6.7.3
- v6.7.2
- v6.7.1
- v6.7.0
- v6.6.0
- v6.5.0
- v6.4.0
- v6.3.3
- v6.3.2
- v6.3.1
- v6.3.0
- v6.2.1
- v6.2.0
- v6.1.0
- v6.0.0
- 5.x-dev
- v5.0.5
- v5.0.4
- v5.0.3
- v5.0.2
- v5.0.1
- v5.0.0
- v4.0.2
- v4.0.1
- v4.0.0
- v3.2.0
- v3.1.0
- v3.0.0
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- 1.x-dev
- v1.0.13
- v1.0.12
- v1.0.10
- v1.0.9
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
This package is auto-updated.
Last update: 2026-11-13 13:11:33 UTC
README
What does it do? 😊
This bundle provides features that help you run your Symfony-framework-based App's testsuite more efficiently with isolated tests.
It provides a StaticDriver that will wrap your originally configured Driver class (like DBAL\Driver\PDOMysql\Driver) and keeps a database connection statically in the current php process.
With the help of a PHPUnit extension class it will begin a transaction before every testcase and roll it back again after the test finished for all configured DBAL connections. This results in a performance boost as there is no need to rebuild the schema, import a backup SQL dump or re-insert fixtures before every testcase. As long as you avoid issuing DDL queries that might result in implicit transaction commits (Like ALTER TABLE, DROP TABLE etc; see https://wiki.postgresql.org/wiki/Transactional_DDL_in_PostgreSQL:_A_Competitive_Analysis) your tests will be isolated and all see the same database state.
It also includes a Psr6StaticArrayCache that will be automatically configured as meta data & query cache for all EntityManagers. This improved the speed and memory usage for my testsuites dramatically! This is especially beneficial if you have a lot of tests that boot kernels (like Controller tests or ContainerAware tests) and use Doctrine entities.
How to install and use this Bundle?
- install via composer
composer require --dev dama/doctrine-test-bundle
- If you're not using Flex, enable the bundle by adding the class to bundles.php
<?php // config/bundles.php return [ //... DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], //... ];
- Starting from version 8 and only when using DBAL < 4 you need to make sure you have
use_savepointsenabled on your doctrine DBAL configuration for all relevant connections:
doctrine: dbal: connections: default: use_savepoints: true
Using the Bundle with PHPUnit
-
Add the Extension to your PHPUnit XML config
<phpunit> ... <extensions> <bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" /> </extensions> </phpunit>
-
Make sure you also have
phpunit/phpunitavailable as adevdependency (versions 11, 12 and 13 are supported with the built-in extension) to run your tests. Alternatively this bundle is also compatible withsymfony/phpunit-bridgeand itssimple-phpunitscript. (Note: you may need to make sure the phpunit-bridge requires the correct PHPUnit 10+ Version using the environment variableSYMFONY_PHPUNIT_VERSION). -
That's it! From now on whatever changes you do to the database within each single testcase (be it a
WebTestCaseor aKernelTestCaseor any custom test) are automatically rolled back for you 😊
Skipping the transactional database connection handling for specific tests
With PHPUnit you can skip this bundle's database rollback handling for specific tests if needed:
#[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() {}
Changes made in setUpBeforeClass() are rolled back before the first test of the class, so its tests do not see them. In a class using #[SkipDatabaseRollback], they are committed like the changes of its tests.
Warning about transactions that were never closed
As 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.
The PHPUnit extension triggers a warning for such tests, so they fail when using failOnWarning="true". This only works for tests with an after-hook like tearDown(), which KernelTestCase and WebTestCase both have. To disable the warning:
<extensions> <bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"> <parameter name="warnAboutOpenTransactions" value="false"/> </bootstrap> </extensions>
Sharing database state between the tests of a class
To share the changes made in setUpBeforeClass() with all tests of the class, add the #[ShareSetUpBeforeClassChanges] attribute: once setUpBeforeClass() ran, the PHPUnit extension creates a savepoint level, and before each test it then only rolls back to this level instead of rolling back everything. The level is dropped when the next test class starts.
use DAMA\DoctrineTestBundle\PHPUnit\ShareSetUpBeforeClassChanges; #[ShareSetUpBeforeClassChanges] // also supported on (abstract) parent classes class MyTest extends KernelTestCase { public static function setUpBeforeClass(): void { // ... insert the rows shared by all tests of this class } }
The attribute does nothing in a class using #[SkipDatabaseRollback], where the changes made in setUpBeforeClass() are committed.
A level can also be created with StaticDriver::createSavepoint(), and levels can be nested: StaticDriver::rollBackToSavepoint() rolls back to the innermost level and keeps it, StaticDriver::rollBackAndReleaseSavepoint() rolls back to it and removes it, and StaticDriver::getSavepointLevel() returns the current one. A level created within a test is not removed when the test ends: remove it in the test, otherwise the following tests of the class keep the changes made before it was created. A level cannot be created while a transaction started by your code is open, nor without the static connections, like in a class using #[SkipDatabaseRollback].
Keep in mind that the shared rows stay locked by their connection for the whole class: another connection writing to the same rows or tables might have to wait for them. This includes a test of the class using #[SkipDatabaseRollback]: it runs on its own connection, which does not see the shared rows and might wait for their locks. The other tests of the class might not see the rows it commits either: the static transaction is not restarted between them, so with an isolation level like REPEATABLE READ, the default of MySQL, it keeps reading from the snapshot it took before.
Keeping the changes of a test for the tests depending on it
A savepoint level can also keep the changes of a test for the tests depending on it with #[Depends]: create it as the last statement of the test, and remove it in the last test depending on it.
use DAMA\DoctrineTestBundle\Doctrine\DBAL\StaticDriver; class MyTest extends WebTestCase { public function testCreatePost(): int { // ... POST /api/posts, assertions StaticDriver::createSavepoint(); // last statement: the changes made after it are rolled back return $id; } #[Depends('testCreatePost')] public function testUpdatePost(int $id): void { // first statement: the post stays visible in this test, and is rolled back after it even if it fails StaticDriver::rollBackAndReleaseSavepoint(); // ... the post still exists } }
Until the level is removed, all tests of the class see the kept changes, not only the ones depending on it: declare the tests depending on another one right after it.
Using the Bundle with Behat
Enable the extension in your Behat config (e.g. behat.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)) );
Or when still using a YAML config with Behat 3 (e.g. behat.yml)
default: # ... extensions: DAMA\DoctrineTestBundle\Behat\ServiceContainer\DoctrineExtension: ~
That's it! From now on whatever changes you do to the database within each scenario are automatically rolled back for you.
Please note that this is only works if the tests are executed in the same process as Behat. This means it cannot work when using e.g. Selenium to call your application.
Configuration
The bundle exposes a configuration that looks like this by default:
dama_doctrine_test: enable_static_connection: true enable_static_meta_data_cache: true enable_static_query_cache: true
Setting enable_static_connection: true means it will enable it for all configured doctrine dbal connections.
You can selectively only enable it for some connections if required:
dama_doctrine_test: enable_static_connection: connection_a: true
Controlling how connections are kept statically in the current php process
By default, every configured doctrine DBAL connection will have its own driver connection that is managed in the current php process. In case you need to customize this behavior you can choose different "connection keys" that are used to select driver connections.
Example for 2 connections that will re-use the same driver connection instance:
doctrine: dbal: connections: default: url: '%database.url1%' foo: url: '%database.url2%' dama_doctrine_test: connection_keys: # assigning the same key will result in the same internal driver connection being re-used for both DBAL connections default: custom_key foo: custom_key
Since v8.1.0: For connections with read/write replicas the bundle will use the same underlying driver connection by default for the primary and also for replicas. This addresses an issue where inconsistencies happened when reading/writing to different connections. This can also be customized as follows:
doctrine: dbal: connections: default: url: '%database.url%' replicas: replica_one: url: '%database.url_replica%' dama_doctrine_test: connection_keys: # assigning different keys will result in separate internal driver connections being used for primary and replica default: primary: custom_key_primary replicas: replica_one: custom_key_replica
Example
An example usage can be seen within the functional tests included in this bundle: https://github.com/dmaicher/doctrine-test-bundle/tree/master/tests
- initial database bootstrap is done using PHPUnit bootstrap file: https://github.com/dmaicher/doctrine-test-bundle/blob/master/tests/bootstrap.php
- several tests that make sure any changes from previous tests are rolled back: https://github.com/dmaicher/doctrine-test-bundle/blob/master/tests/Functional/PhpunitTest.php
This bundle is also used on the official Symfony Demo testsuite: https://github.com/symfony/demo
Debugging
Sometimes it can be useful to be able to debug the database contents when a test failed. As normally all changes are rolled back automatically you can do this manually:
public function testMyTestCaseThatINeedToDebug() { // ... something thats changes the DB state \DAMA\DoctrineTestBundle\Doctrine\DBAL\StaticDriver::commit(); die; // now the DB changes are actually persisted and you can debug them }
Troubleshooting
In case you are running (maybe without knowing it) queries during your tests that are implicitly committing any open transaction (see https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html for example) you might see an error like this:
Doctrine\DBAL\Driver\PDOException: SQLSTATE[42000]: Syntax error or access violation: 1305 SAVEPOINT DOCTRINE2_SAVEPOINT_2 does not exist
Currently there is no way for this bundle to work with those queries as they simply cannot be rolled back after the test case finished.
See also #58