php - "There is no active transaction" when refreshing database in Laravel 8.0 test

Solution:

The issue is that your test contains implicit commits and so ends the active transaction.

Re -

"As soon as I do a CREATE statement I get the transaction error." -

CREATE TABLE etc are statements that cause an implicit commit.

This in turn means the RefreshDatabase trait will not work because a rollback is not possible when the transaction is closed.

Hence PDOException: There is no active transaction

It seems to be a known issue/thrown error with php 8.0 - https://github.com/laravel/framework/issues/35380

Answer

Solution:

I figured it out for the case I was having. I am using a library wich uses the migration logic to execute data migrations (hence the $this->artisan('migrate-data'); in my code).
When using the RefreshDatabase trait, migrations are executed once and after that, a transaction is started. The migration logic does something with transactions, I think closing them afterwards, causing the error I was having.

The solution that worked for me, was to overwrite the RefreshDatabase trait to execute the data migrations before starting the transaction:

<?php

namespace Tests;

use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\RefreshDatabaseState;

trait RefreshDatabaseWithData
{
    use RefreshDatabase;

    protected function refreshTestDatabase(): void
    {
        if (! RefreshDatabaseState::$migrated) {
            $this->artisan('migrate:fresh', $this->migrateFreshUsing());
            $this->artisan('migrate-data'); // << I added this line

            $this->app[Kernel::class]->setArtisan(null);

            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
     }
}

This is tested in Laravel 9 only, but I don't see why this wouldn't work on previous versions.

Answer

Solution:

This is how I solved it:

Step 1: Extend Connection class and override transaction method:

use Illuminate\Database\Connection as DBConnection;
use Closure;

class Connection extends DBConnection
{

    public function transaction(Closure $callback, $attempts = 1)
    {
        $callback($this);
    }
}

This override basically disables transactions internally, keeping your current code intact.

Step 2: Bind that concrete class to the abstract ConnectionInterface via a ServiceProvider (app/Providers/AppServiceProvider.php would be a good place):

if(config('app.env')==='testing') {
   $this->app->bind(ConnectionInterface::class, Connection::class);
}

Notice that the binding is inside a condition against the environment. We want to apply this binding only when running tests.

If you're not using a .env.testing file for Unit Testing, I really recommend you to do it. It keeps everything much more cleaner. You can just copy .env to .env.testing and only update the DB_CONNECTION and DB_DATABASE constants, pointing to your test database.

Then define the .env.testing environment in phpinit.xml:

<php>
   <env name="APP_ENV" value="testing"/>
   <!-- <env name="DB_CONNECTION" value="memory_testing"/> -->
   <!-- <env name="DB_DATABASE" value=":memory:"/> -->
</php>

Notice that I commented out DB_CONNECTION AND DB_DATABASE since those parameters will be fetched from .env.testing

Source