“没有主动交易”在Laravel 8.0测试中刷新数据库时

发布于 2025-02-01 18:48:32 字数 3590 浏览 3 评论 0原文

使用PHP 8.0.2和Laravel 8.37.0,我正在运行测试,每个测试都应在其中刷新数据库数据,因为每个测试存在冲突的数据(由于唯一的约束)。
使用SQLite的内存数据库,这有效,但是当我切换到MySQL(v8.0.23)时,我会收到下一个错误:

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

并且由于已经插入了数据并且在测试后未清除该数据后的测试失败。

我要做的测试是:

<?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',
            ]
       ]);
    }
}

执行此测试并在数据库中检查后,数据仍然存在。有或没有行受保护的数组$ ConnectionStotRansact = ['mySql'];给出了相同的结果。

我的phpunit.xml - file看起来像:

<?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>

这是一个已知问题吗?还是我想念某个时候?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

时光磨忆 2025-02-08 18:48:32

问题是您的测试包含隐式提交,因此结束了主动交易。

关于 -

“一旦我做一个创建语句,就会得到交易错误。” -

创建表等隐含的提交。

反过来,这意味着RefreshDatabase性状将无法使用,因为当交易关闭时不可能回滚。

因此,pdoException:没有主动事务

,似乎是已知的问题/抛出的错误,php 8.0- https://github.com/laravel/framework/issues/35380

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

扛起拖把扫天下 2025-02-08 18:48:32

我想知道的是我的情况。我正在使用库的库使用迁移逻辑来执行数据迁移(因此,我的代码中的$ this-&gt; artisan('migrate-data');在我的代码中)。
当使用RefreshDatabase特征时,一次又一次执行迁移,开始进行事务。迁移逻辑对交易有所作为,我认为之后将它们关闭,从而导致我遇到的错误。

对我有用的解决方案是覆盖RefreshDatabase特征以在开始交易之前执行数据迁移:

<?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();
     }
}

仅在Laravel 9中进行测试,但我不明白为什么这不起作用在以前的版本上。

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.

黎夕旧梦 2025-02-08 18:48:32

这就是我解决的方式:

步骤1:扩展连接类和覆盖事务方法:

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

class Connection extends DBConnection
{

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

替代基本上会在内部禁用交易,使您的当前代码保持完整。

步骤2:通过ServiceProvider(App/Provers/AppServiceProvider.php)将该混凝土类绑定到抽象连接系数是一个好地方):

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

请注意,绑定在对环境的条件内。我们只想在运行测试时仅应用此绑定。

如果您不使用.env.testing文件进行单元测试,我真的建议您这样做。它使一切都更加干净。您可以将.env复制到.env.testing,并且只能更新DB_Connection和DB_DATABASE常数,指向您的测试数据库。

然后在phpinit.xml中定义.env.testing环境:

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

请注意,由于这些参数将从.env.testing获取,因此我评论了DB_Connection和db_database

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文