phplaraveltestingphpunitlaravel-10

Laravel RefreshDatabase trait not wrapping each test in a transaction


RefreshDatabase docs states that

The Illuminate\Foundation\Testing\RefreshDatabase trait does not migrate your database if your schema is up to date. Instead, it will only execute the test within a database transaction.

I'm on Laravel 10 and I think it is not working properly in my project.

Problem #1

Since the project migrations take ~1m to be executed (php artisan migrate:fresh --env testing) I have a local testing db that I keep up to date to run tests quickly, but tests execution always took at least 1m as if it is running all the migrations before running the tests.

Problem #2

I need to test that a piece code falls into a catch(Exception $e) branch and since this piece of code retrieve results from the db I thought to alter the table name after all the setUp to make it throw an exception, and that worked.
Problem is, now every subsequent test in the same test class that tests the same piece of code fails with the message SQLSTATE[42S02]: Base table or view not found: this means the table still have the new wrong name, so it looks like it is not the single test to be wrapped in a migration but all the tests in the same test class, but this conflicts with the previous db operation (e.g. instances being created) not affecting subsequent tests.

I really cannot figure out what's not working properly.


Solution

  • I am leaving this as an answer instead of a comment so I can put code and make it more visible:

    You are correct about the docs being misleading, have in mind that your original post was linking Laravel 11.x docs instead of 10.x (I have fixed that) but they still say the same thing.

    This is the source code for Laravel 10:

    public function refreshDatabase()
    {
        $this->beforeRefreshingDatabase();
    
        $this->usingInMemoryDatabase()
                        ? $this->refreshInMemoryDatabase()
                        : $this->refreshTestDatabase();
    
        $this->afterRefreshingDatabase();
    }
    

    As you can see you have a before hook, the migration runs, and an after hook. Both hooks are empty, you can add code in your test to interfere with the trait.

    If you check what refreshTestDatabase() method executes:

    protected function refreshTestDatabase()
    {
        if (! RefreshDatabaseState::$migrated) {
            $this->artisan('migrate:fresh', $this->migrateFreshUsing());
    
            $this->app[Kernel::class]->setArtisan(null);
    
            RefreshDatabaseState::$migrated = true;
        }
    
        $this->beginDatabaseTransaction();
    }
    

    You can see it is making use of RefreshDatabaseState, it is a simple static class, and it is not set beforehand or manipulated in any way. So RefreshDatabaseState::$migrated is always false on the first run, so it will run the code inside it, in this case a migrate:fresh.

    I have never took your approach, but you can try getting in the before hook and you manually check if you have all migrations run or not, something like this:

    // In your test define exactly this
    protected function beforeRefreshingDatabase()
    {
        $this->artisan('migrate');
    
        \Illuminate\Foundation\Testing\RefreshDatabaseState::$migrated = true;
    }
    

    This way we are migrating the database automatically (if any run is missing), and we then tell the trait that it was already done, no need to migrate:fresh. Have in mind that it may not exactly work as it is then running $this->app[Kernel::class]->setArtisan(null); and I have no idea why and I really don't want to run it.

    I thought of using migrate:status and check if all migrations were run, but it is not giving us a status code if any is missing, so it is harder as you would need to read the output and check it, something not really desired.


    The simplified solution is:

    If you want this solution, just include DatabaseTransactions in your test but remember to either automatically run migrate on setUp in your base test class or always be up-to-date by CLI, that's it!