Laravel

Under the hood: How RefreshDatabase works in Laravel tests

Daniel Verner -

Introduction
When writing tests for any application it is crucial to every test run independently without affecting each other. If your tests use a database, every test should run with a clean database. By clean I mean a known state, fresh migrated or seeded, not necessarily an empty one. Laravel offers an easy way to handle this using the RefreshDatabase trait. For more information about the usage of the trait please check the documentation here.

A test’s life cycle in Laravel
Before we dive into the details of how to refresh database works, let’s take a look at the test’s life cycle. Laravel ships with PHPUnit, so the life cycle of the tests is very similar to the PHPUnit tests life cycle. I won’t cover the details of the whole test running process, just stick to the points which are interesting for the current topic:

  • Before every test the setUp method is running:
    • It creates/refreshes the laravel application
    • Sets up traits
    • Calls the afterApplicationCreatedCallbacks, sets up events, clears facade’s resolved instances, etc.
  • The actual test is running
  • After every test the tearDown method is running:
    • It calls the beforeApplicationDestroyedCallbacks
    • Closes Mockery resets variables etc.

The RefreshDatabase trait
If the test uses the RefreshDatabase trait, the setUpTraits calls the refreshDatabase() method from the trait, and the interesting part starts here. In tests, you can use in-memory and regular databases, depending on how you’ve set up the test environment, it will refresh the database accordingly.

The in-memory database
In the case of in-memory database things are quite simple just migrates the database, it runs very fast, so it can be done before every test. Easy enough.
protected function refreshInMemoryDatabase()
{
    $this->artisan('migrate');
 
    $this->app[Kernel::class]->setArtisan(null);
}

Regular database
In other cases, it only migrates the database, if it has not been migrated e.g. before running the first test.
        if (! RefreshDatabaseState::$migrated) {
            $this->artisan('migrate:fresh', [
                '--drop-views' => $this->shouldDropViews(),
                '--drop-types' => $this->shouldDropTypes(),
            ]);
 
            $this->app[Kernel::class]->setArtisan(null);
 
            RefreshDatabaseState::$migrated = true;
        }
When the database has been migrated it starts a database transaction:
        foreach ($this->connectionsToTransact() as $name) {
            $connection = $database->connection($name);
            $dispatcher = $connection->getEventDispatcher();
 
            $connection->unsetEventDispatcher();
            $connection->beginTransaction();
            $connection->setEventDispatcher($dispatcher);
        }
and registers a beforeApplicationDestroyed callback where the transaction is rolled back. These callbacks are running in the tearDown method, so after every test. The rollback ensures that the database isn’t changed by the test, and the next test can run on a clean database.
        $this->beforeApplicationDestroyed(function () use ($database) {
            foreach ($this->connectionsToTransact() as $name) {
                $connection = $database->connection($name);
                $dispatcher = $connection->getEventDispatcher();
 
                $connection->unsetEventDispatcher();
                $connection->rollback();
                $connection->setEventDispatcher($dispatcher);
                $connection->disconnect();
            }
        });

Potential pitfalls
While the method described above works fine in most cases, I had some cases when I spent a couple of hours searching, debugging why my tests are failing and the database doesn’t get refreshed correctly.

As you might presume, the solution is NOT to commit the opened database transaction in tests, and avoid using statements which cause an implicit commit in MySQL. I made the second mistake by running an SQL dump in one of my seeders, which created a table, and therefore implicitly committed the transaction. You can find out more about implicit commits in the MySQL documentation. Self-defense on: I know the database tables should be created with migrations, and seeders should be written with factories, but sometimes we need to do non-optimal things :-).

Conclusion
The RefreshDatabase is a very useful feature when writing tests for Laravel application, just need to be careful with transaction commits in your tests.

Hope this article was useful, if you have any additions, remarks, please let me know in the comments section.

Related articles
If you like this article, please check the other posts of the series here.

Tags: Laravel · testing · under the hood

Want products news and updates?

Sign up for our newsletter to stay up to date.

We care about the protection of your data. Read our Privacy Policy.

Impressions from our Team

  • Happy birthday 🎁🎈🎂 Filip - #

  • Another day another #mandarinacakeshop 🎂 😀 - #

  • Happy Birthday Ognjen! And marry Christmas to all other 🎄#notacakeshop - #

  • #Office #Garden - #

  • #workhard - #

  • #belgrade #skyline - #

  • #happybirthday Phil :) - #

  • #happybirthday Stefan 🥂 - #

  • #happybirthday Lidija 🍾 - #

  • Say hi 👋 to our newest team member ☕️ - #

  • #bithday #cake 😻 - #

  • #stayathome #homeoffice #42coders - #

  • #stayathome #homeoffice #42coders #starwars :) - #

  • #stayathome #homeoffice #42coders - #

  • We had a really nice time with #laracononline #laravel - #

  • Happy Birthday 🎂 Miloš - #

  • Happy Birthday 🎂Nikola - #

  • #42coders #christmas #dinner what a nice evening :) - #

  • Happy Birthday 🎂 Ognjen - #

  • Wish you all a merry Christmas 🎄🎁 - #

See more!

© 2024 42coders All rights reserved.