Reusing Databases
It takes time to build a database, and this can slow down your tests. Particularly if it needs to be rebuilt multiple times during a test-run, and at worst, for every test.
The way to get the fastest speed out of your database is to re-use it. This way, it only needs to be built the first time.
For this to be possible, the database needs to be returned to its initial state after each test completes. This is important because tests need to be deterministic. If changes are left-over, following tests might act differently, becoming unstable.
Adapt uses Transaction and Journaling methods to roll-back changes made during a test.
Once rolled-back, Adapt checks to make sure the roll-back was successful before reusing a database. When it's not safe to, it will be rebuilt next time.
As well as re-using databases between tests, Adapt reuses databases between test-runs. This means that your tests can start straight away the next time you run your tests.
Transactions
When it's possible to use them, transactions are the quickest and preferred method of returning a database to its original state.
Like Laravel's RefreshDatabase
and DatabaseTransactions
traits, Adapt wraps each test inside a transaction. This transaction is rolled-back afterwards to undo any changes.
Transaction re-use is enabled by default. You can turn it off by updating Adapt's reuse_methods.transactions
config setting. Or you can choose per-test by adding the $transactions
property to your test-classes.
⚙️ See configuration
# .env.testing
ADAPT_REUSE_TRANSACTIONS=true
or
<?php
// config/code_distortion.adapt.php
return [
…
'reuse_methods' => [
'transactions' => env('ADAPT_REUSE_TRANSACTIONS', true), // or false
…
],
…
];
or
<?php
// tests/Feature/MyFeatureTest.php
use CodeDistortion\Adapt\AdaptDatabase;
use Tests\TestCase;
class MyFeatureTest extends TestCase
{
use AdaptDatabase;
protected bool $transactions = true; // or false
…
}
There are some situations where transactions can't be used, however, including:
- When running browser tests - database changes within the transaction cannot be seen when responding to the browser's request.
- When your tests have their own transactions - nested transactions aren't supported by MySQL, PostgreSQL or SQLite.
- Accidental commits - changing the database structure will implicitly commit MySQL transactions, for example.
Adapt will throw an exception if its "wrapper" transaction is committed (or rolled-back) during your test. When this happens, the database will be rebuilt the next time a test needs it. In these situations it's best to turn transaction re-use off.
NOTE
Adapt detects when a Dusk browser test is being run, and disables transaction re-use automatically.
TIP
Auto-increment ids in MySQL won't be reset back to their original values. New ids will continue to increase between tests. For this reason it's important not to make your tests reliant on particular ids.
Journaling
Adapt can "roll-back" database changes made during a test in a more manual way. By keeping track of the changes, Adapt can apply them in reverse-order afterwards.
WARNING
This method is new and experimental. It is currently only available for MySQL databases.
This alternative is slower than when using transactions, but still generally faster than rebuilding the database. And it can be used when transactions cannot (e.g. when running browser tests).
If the transaction method is enabled, transaction takes precedence over journaling, and will be used instead.
Journaling re-use is disabled by default (as it's experimental). But you can turn it on by updating Adapt's reuse_methods.journals
config setting. You can also use it per-test by adding the $journals
property to your test-classes.
⚙️ See configuration
# .env.testing
ADAPT_REUSE_JOURNALS=true
or
<?php
// config/code_distortion.adapt.php
return [
…
'reuse_methods' => [
…
'journals' => env('ADAPT_REUSE_JOURNALS', true), // or false
…
],
…
];
or
<?php
// tests/Feature/MyFeatureTest.php
use CodeDistortion\Adapt\AdaptDatabase;
use Tests\TestCase;
class MyFeatureTest extends TestCase
{
use AdaptDatabase;
protected bool $journals = true; // or false
…
}
If journaling cannot restore the database back to its original state, an exception will be thrown. When this happens, the database will be rebuilt the next time a test needs it. In these situations it's best to turn journaling re-use off.
NOTE
Journaling will only be applied to databases where it's available (i.e. MySQL), and is ignored otherwise.
Some notes about journaling:
- Tables being rolled-back need to have a primary-key or unique-index.
- Changes made to the structure of tables by a test won't always be detected. It may let them pass without error.
- Rows are removed and re-inserted during this process. This may cause the order of results returned by queries to change. However, you shouldn't rely on the order of results returned by databases without specifying the order direction explicitly anyway.
WARNING
- You may run in to issues if your tests change a large amount of data during a test. Currently, the whole set of changed data is loaded per-table into PHP memory while being rolled-back. This may be addressed in a future release.
- At the moment, journaling may act strangely when your database contains triggers. This may be addressed in a future release.
TIP
Just like transaction-based re-use, auto-increment ids in MySQL won't be reset back to their original values. New ids will continue to increase between tests. For this reason it's important not to make your tests reliant on particular ids.
Verifying that Journaling Works
To add extra assurance that journaling is working correctly (seeing it's experimental), a new method has been added as a safety-check, to verify the structure and content of databases after each test.
This process takes extra time to run, so is disabled by default. You can turn it on by setting Adapt's verify_databases
config setting to true.
⚙️ See configuration
# .env.testing
ADAPT_VERIFY_DATABASES=true
or
<?php
// config/code_distortion.adapt.php
return [
…
'verify_databases' => env('ADAPT_VERIFY_DATABASES', true), // or false
…
];
An exception will be thrown if:
- a table was added or removed,
- the structure of a table has changed,
- or the data inside a table changed.
When this happens, the database will be rebuilt.
WARNING
This does not currently look at other things that might change, like views, triggers or stored-procedures. These may be addressed in a future release.
Snapshots
After a database has been built, Adapt can take a snapshot of it (i.e. create a sql-dump file). Then, the next time the database needs to be built, this is imported automatically instead, skipping the normal build steps.
TIP
There's a fair chance that you won't need snapshots, however it might be useful if your migrations and seeders aren't optimal, and the above methods of reusing your database can't be used.
Snapshots can be taken at different points in the building process:
"afterMigrations"
A snapshot is taken straight after initial-imports + migrations are run, but before seeding."afterSeeders"
A snapshot is taken at the end, after seeders have run."both"
A snapshot is taken after initial-imports + migrations, and after seeding.false
Turns the snapshot feature off.
NOTE
Adapt only applies snapshots if the other methods of re-using a database above aren't used. You can override this and have Adapt take snapshots anyway, by adding a !
prefix. i.e. "!afterMigrations"
, "!afterSeeders"
, "!both"
.
You can choose the snapshots setting to use by updating Adapt's reuse_methods.snapshots
config value, or by adding the $snapshots
property to your test-classes.
⚙️ See configuration
# .env.testing
ADAPT_REUSE_SNAPSHOTS=afterSeeders
or
<?php
// config/code_distortion.adapt.php
return [
…
'reuse_methods' => [
…
// "afterMigrations", "afterSeeders", "both"
// "!afterMigrations", "!afterSeeders", "!both"
// or false
'snapshots' => env('ADAPT_REUSE_SNAPSHOTS', 'afterSeeders'),
…
],
…
];
or
<?php
// tests/Feature/MyFeatureTest.php
use CodeDistortion\Adapt\AdaptDatabase;
use Tests\TestCase;
class MyFeatureTest extends TestCase
{
use AdaptDatabase;
// "afterMigrations", "afterSeeders", "both"
// "!afterMigrations", "!afterSeeders", "!both"
// or false
protected string|bool $snapshots = 'afterSeeders';
…
}