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

Using php 8.0.2 and Laravel 8.37.0, I am running tests where for every test the database data should be refreshed, since there is conflicting data per test (due to unique constraints).
using the in-memory database with SQLite, this works, but when I switch to MySQL (v8.0.23) I get the next error:

1) Tests\Feature\Controllers\AuthControllerTest::testSuccessLogin
PDOException: There is no active transaction

and the tests after this one fail due to data already inserted and not cleared after the test.

The the test that I am trying to do is:

<?php

namespace Tests\Feature\Controllers;

use App\Models\User;
use App\Models\User\Company;
use App\Repositories\UserRepository;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AuthControllerTest extends TestCase
{
    use RefreshDatabase;

    protected array $connectionsToTransact = ['mysql'];

    public function testSuccessLogin(): void
    {
        $this->artisan('migrate-data');

        /** @var User $user */
        $user = User::factory()->create([
            'email' => '[email protected]'
        ]);

        $this->app->bind(UserRepository::class, function() use ($user) {
            return new UserRepository($user, new Company());
        });

        $loginResponse = $this->post('/api/login', [
            'email' => '[email protected]',
            'password' => 'password'
        ]);

        $loginResponse->assertStatus(200);
        $loginResponse->assertJsonStructure([
            'data' => [
                'user' => [
                    'name',
                    'surname',
                    'email',
                    'abilities'
                ],
                'token',
            ]
       ]);
    }
}

and after executing this test and checking in the database, the data still exists. With and without the line protected array $connectionsToTransact = ['mysql']; gives me the same result.

My phpunit.xml-file look like:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
     bootstrap="vendor/autoload.php"
     colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
        <report>
            <html outputDirectory="reports/coverage"/>
        </report>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="mysql"/>
        <server name="DB_HOST" value="localhost"/>
        <server name="DB_DATABASE" value="mysql_test"/>
        <server name="DB_USERNAME" value="root"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

Is this a known issue? Or am I missing someting?



Solution 1:[1]

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

Solution 2:[2]

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 John Smith