Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[11.x] Re-refresh the database if the RefreshDatabase transaction was committed #53997

Merged
merged 1 commit into from
Dec 26, 2024

Conversation

SjorsO
Copy link
Contributor

@SjorsO SjorsO commented Dec 22, 2024

The RefreshDatabase trait:

  • Runs migrate:fresh before the first test
  • Opens a database transaction before each test
  • Rolls back that transaction after each test

The issue is that it is possible to accidentally commit the transaction started by RefreshDatabase. For example, truncating a table automatically commits any active transactions. If a test commits the transaction, all database records created by that test won't be rolled back, causing each subsequent test to have a dirty database.

This is especially frustrating to debug because the test that commits the transaction passes, but tests after that fail unpredictably depending on the type of assertions they do. The logical place to start debugging the issue is with the failing tests, but those have nothing to do with the problem.

This PR solves this issue by refreshing the database if you (accidentally) commit the database transaction started by the RefreshDatabase trait.

To reproduce this issue, use MySQL, then run the following tests:

class ExampleTest extends TestCase
{
    use RefreshDatabase; // or LazilyRefreshDatabase

    public function test_the_first_test()
    {
        User::truncate();

        User::factory()->create();

        $this->expectNotToPerformAssertions();
    }

    public function test_the_second_test()
    {
        // This test passes if you run it in isolation, but it fails if you run all tests
        $this->assertSame(0, User::count());
    }
}

@SjorsO SjorsO marked this pull request as draft December 22, 2024 15:55
@SjorsO SjorsO force-pushed the refresh-database-prevent-footgun branch from 0c024bf to f19b479 Compare December 22, 2024 16:22
@SjorsO SjorsO changed the title [11.x] Throw exception if RefreshDatabase trait is not in a transaction [11.x] Re-refresh the database if RefreshDatabase's transaction was committed Dec 22, 2024
@SjorsO SjorsO changed the title [11.x] Re-refresh the database if RefreshDatabase's transaction was committed [11.x] Re-refresh the database if the RefreshDatabase transaction was committed Dec 22, 2024
@SjorsO SjorsO marked this pull request as ready for review December 22, 2024 16:25
@taylorotwell taylorotwell merged commit 8636996 into laravel:11.x Dec 26, 2024
38 checks passed
@taylorotwell
Copy link
Member

Is it possible to add a test for this in some way?

@chrisnetonline
Copy link

I think there might be an issue introduced here. Post upgrade I am seeing the following error:

Error: Call to a member function inTransaction() on null
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/RefreshDatabase.php:112
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php:285
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php:113
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:66

@SjorsO
Copy link
Contributor Author

SjorsO commented Jan 3, 2025

I think there might be an issue introduced here. Post upgrade I am seeing the following error:

Error: Call to a member function inTransaction() on null
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/RefreshDatabase.php:112
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php:285
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php:113
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:66

Which database are you using for your tests?

@chrisnetonline
Copy link

@SjorsO MySQL Docker Image 8.0.32-1.2.11-server

@SjorsO
Copy link
Contributor Author

SjorsO commented Jan 3, 2025

@SjorsO MySQL Docker Image 8.0.32-1.2.11-server

Could you manually edit the RefreshDatabase trait in your project to see if the code below solves the issue?

if ($connection->getPdo() && ! $connection->getPdo()->inTransaction()) {
    RefreshDatabaseState::$migrated = false;
}

If it does, I'll open up a PR to get this breaking change fixed (although I have no clue why ->getPdo() would be null in your case).

@chrisnetonline
Copy link

@SjorsO MySQL Docker Image 8.0.32-1.2.11-server

Could you manually edit the RefreshDatabase trait in your project to see if the code below solves the issue?

if ($connection->getPdo() && ! $connection->getPdo()->inTransaction()) {
    RefreshDatabaseState::$migrated = false;
}

If it does, I'll open up a PR to get this breaking change fixed (although I have no clue why ->getPdo() would be null in your case).

That does indeed fix the issue.

@chrisnetonline
Copy link

chrisnetonline commented Jan 3, 2025

I can't share a ton of code, but our base "Feature" test case uses the DatabaseTransactions trait and then we supply a list of database connections that we have transactions on. The app has multiple connections so maybe that is somehow related:

    protected array $connectionsToTransact = [
        DatabaseConnection::CONNECTION1,
        DatabaseConnection::CONNECTION2,
        DatabaseConnection::CONNECTION3,
    ];

@SjorsO
Copy link
Contributor Author

SjorsO commented Jan 4, 2025

I've opened a PR to fix the breaking change: #54075

You mentioned that you're using DatabaseTransactions, but that trait is separate from RefreshDatabase, so does that mean you're using both?

@chrisnetonline
Copy link

@SjorsO All of our feature tests use a base class that includes DatabaseTransactions. This one test class happened to also include RefreshDatabase, so yes, in this case, both traits were in use. The RefreshDatabase trait didn't actually need to be used so we have since removed it.

@27pchrisl
Copy link

@chrisnetonline I had the same issue, our system uses https://github.com/archtechx/tenancy. It might be because connectionsToTransact() returns multiple database connections that are defined and could support transactions, but one was never actually used and so pdo was never initialized for that connection. The change in #54075 fixed the issue for me too.

@27pchrisl
Copy link

Woops I actually meant @SjorsO ! ;)

@oranges13
Copy link

We are using sqlite in our test cases and have the same Error: Call to a member function inTransaction() on null glad to see it fixed above, but adding just in case someone else finds themselves here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants