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

RefreshDatabase not refreshing DB #53758

Open
katzda01 opened this issue Dec 4, 2024 · 12 comments
Open

RefreshDatabase not refreshing DB #53758

katzda01 opened this issue Dec 4, 2024 · 12 comments

Comments

@katzda01
Copy link

katzda01 commented Dec 4, 2024

Laravel Version

11.31

PHP Version

8.3

Database Driver & Version

:memory:

Description

After upgrading to latest Laravel 11, my feature tests started to fail due to RefreshDatabase trait not refreshing the DB.

Adding RefreshDatabaseState::$migrated = false; to this function fixed the issue. I'm not sure what changed in the framework from last version but running each test individually passes but together they fail on "table does not exist" error.

    Illuminate\Foundation\Testing\RefreshDatabase.php

    protected function restoreInMemoryDatabase()
    {
        $database = $this->app->make('db');

        foreach ($this->connectionsToTransact() as $name) {
            if (isset(RefreshDatabaseState::$inMemoryConnections[$name])) {
                $database->connection($name)->setPdo(RefreshDatabaseState::$inMemoryConnections[$name]);
            }
        }
        RefreshDatabaseState::$migrated = false;
    }

Having this in the base tests/TestCase.php class fixes it as a workaround.

    public function setUp(): void
    {
        parent::setUp();

        RefreshDatabaseState::$migrated = false;
    }

Steps To Reproduce

I think the problem arrizes when using multiple DB connections for different models. These models override default connection with

protected $connection = 'mysql1'; // this is default
protected $connection = 'mysql2'; // this is used in User model

And so in the tests, we have to use these hardcoded connections, which is why for testing we override just what matters

phpunit.xml

<server name="DB_FIRST_DRIVER" value="sqlite"/>
<server name="DB_FIRST_DATABASE" value=":memory:" />
<server name="DB_SECOND_DRIVER" value="sqlite"/>
<server name="DB_SECOND_DATABASE" value=":memory:"/>

full example

.env

APP_NAME=ExampleApp
APP_KEY=<generate something>
APP_DEBUG=true

User model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $connection = 'SECOND';
}

config/database.php

   'default' => env('DB_CONNECTION', 'FIRST'),

    'connections' => [
      'FIRST' => [
          'host' => env('DB_FIRST_WRITE_HOST', '127.0.0.1'),
          'port' => env('DB_FIRST_WRITE_PORT', '3306'),
          'username' => env('DB_FIRST_WRITE_USERNAME', ''),
          'password' => env('DB_FIRST_WRITE_PASSWORD', ''),
          'sticky' => env('DB_FIRST_STICKY', false),
          'driver' => env('DB_FIRST_DRIVER', 'mysql'),
          'database' => env('DB_FIRST_DATABASE', 'FIRST'),
          'unix_socket' => env('DB_FIRST_SOCKET', ''),
          'charset' => 'utf8mb4',
          'collation' => 'utf8mb4_unicode_ci',
          'prefix' => '',
          'prefix_indexes' => true,
          'strict' => true,
          'engine' => null,
          'options' => extension_loaded('pdo_mysql') ? array_filter([
              PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
          ]) : [],
      ],

      'SECOND' => [
          'host' => env('DB_SECOND_READ_HOST', '127.0.0.1'),
          'port' => env('DB_SECOND_READ_PORT', '3306'),
          'username' => env('DB_SECOND_READ_USERNAME', ''),
          'password' => env('DB_SECOND_READ_PASSWORD', ''),
          'sticky' => env('DB_SECOND_STICKY', false),
          'driver' => env('DB_SECOND_DRIVER', 'mysql'),
          'database' => env('DB_SECOND_DATABASE', 'SECOND'),
          'unix_socket' => env('DB_SECOND_SOCKET', ''),
          'charset' => 'utf8mb4',
          'collation' => 'utf8mb4_unicode_ci',
          'prefix' => '',
          'prefix_indexes' => true,
          'strict' => true,
          'engine' => null,
          'options' => extension_loaded('pdo_mysql') ? array_filter([
              PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
          ]) : [],
      ],
    ],
<?php

namespace Tests\Feature;

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

class ExampleTest extends TestCase
{
    use RefreshDatabase;

    private User $user;

    public function setUp(): void
    {
        parent::setUp();

        $this->user = User::factory()->create([
          'id' => 1
        ]);
    }

    public function testUnauthenticated()
    {
        $this->assertTrue(true);
    }

    public function testUsersCannotShareReportIdViaCache()
    {
        $this->assertTrue(true);
    }
}
<?php

namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabaseState;

abstract class TestCase extends BaseTestCase
{
  protected function setUp(): void
  {
      parent::setUp();
      // RefreshDatabaseState::$migrated = false;    //uncomment and all tests will pass!
  }
}

migration

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::connection('SECOND')->create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down(): void
    {
    }
};

phpunit.xml

<?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>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>app</directory>
        </include>
    </source>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_STORE" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <server name="DB_FIRST_DRIVER" value="sqlite"/>
        <server name="DB_FIRST_DATABASE" value=":memory:" />
        <server name="DB_SECOND_DRIVER" value="sqlite"/>
        <server name="DB_SECOND_DATABASE" value=":memory:"/>
    </php>
</phpunit>

User factory

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * The current password being used by the factory.
     */
    protected static ?string $password;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => static::$password ??= Hash::make('password'),
            'remember_token' => Str::random(10),
        ];
    }

    /**
     * Indicate that the model's email address should be unverified.
     */
    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}

@crynobone
Copy link
Member

@stancl I believe your recent #53635 PR should be able to solve this.

@katzda01 can you check #53635 as well

@crynobone
Copy link
Member

<server name="DB_FIRST_DRIVER" value="sqlite"/>
<server name="DB_FIRST_DATABASE" value="file:first?mode=memory" />
<server name="DB_SECOND_DRIVER" value="sqlite"/>
<server name="DB_SECOND_DATABASE" value="file:second?mode=memory"/>

Something like above.

@stancl
Copy link
Contributor

stancl commented Dec 5, 2024

Possibly, if the connections are being disconnected at some point. The "table does not exist" errors seem to indicate that.

FWIW the DSN string should possibly also include &cache=shared, not sure. The docs are a bit unclear on that.

It's an old feature for improved read performance (but potentially inconsistent writes?) that isn't recommended anymore but every mention of mode=memory I saw also used cache=shared. So my understanding is that it's discouraged with actual files but since the docs examples for mode=memory used it, I do too.

@katzda01
Copy link
Author

katzda01 commented Dec 5, 2024

@crynobone
I checked out 11.x-dev branch into which #53635 was merged and

First of all that did NOT resolve the issue.

I ran my unit tests with this flag and without.
RefreshDatabaseState::$migrated = false;

With: ..E....E.FEEEE.E.. errors unrelated to this
Without: .EEEEEEEEEEEEEEEEE
All errors say "General error: 1 no such table: user...."

Not to mention I'm getting this error after composer update:

Could not scan for classes inside "/srv/vendor/laravel/framework/src/Illuminate/Queue/IlluminateQueueClosure.php" which does not appear to be a file nor a folder

In ClassMapGenerator.php line 131:

Could not scan for classes inside "/srv/vendor/laravel/framework/src/Illuminate/Queue/IlluminateQueueClosure.php" which does not appear to be a file nor a folder

@crynobone
Copy link
Member

Hey there, thanks for reporting this issue.

We'll need more info and/or code to debug this further. Can you please create a repository with the command below, commit the code that reproduces the issue as one separate commit on the main/master branch and share the repository here?

Please make sure that you have the latest version of the Laravel installer in order to run this command. Please also make sure you have both Git & the GitHub CLI tool properly set up.

laravel new bug-report --github="--public"

Do not amend and create a separate commit with your custom changes. After you've posted the repository, we'll try to reproduce the issue.

Thanks!

@katzda01
Copy link
Author

katzda01 commented Dec 5, 2024

@crynobone In the "Steps to reproduce" I've shared full content of all the files changed to reproduce the issue. Can you first try to reproduce it with the information I provided? I did use the latest laravel installer. I would've submitted a PR straight away here but I don't have write access.

@crynobone
Copy link
Member

Could not scan for classes inside "/srv/vendor/laravel/framework/src/Illuminate/Queue/IlluminateQueueClosure.php" which does not appear to be a file nor a folder

I don't have a way to run a fresh Laravel installation and replicate this. When it exists and has nothing to do with the above "Step to reproduce"

@stancl
Copy link
Contributor

stancl commented Dec 5, 2024

The PR is already part of latest Laravel, no need to change your composer dependencies. Just try what crynobone suggested.

@FurkiFor

This comment was marked as off-topic.

@joelbladt
Copy link

Your phpunit.xml file uses <server> tags to configure variables (e.g., DB_FIRST_DRIVER), but Laravel expects these settings to be defined as environment variables using <env> tags. This difference is important because Laravel retrieves its configuration from .env files or directly from <env> tags in the phpunit.xml file. The <server> tags are handled by PHP itself, but Laravel ignores them because it specifically looks for environment variables.

Fix: Replace the <server> tags with <env> tags so Laravel can recognize these variables as intended:

<?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>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>app</directory>
        </include>
    </source>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_STORE" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="DB_FIRST_DRIVER" value="sqlite"/>
        <env name="DB_FIRST_DATABASE" value=":memory:"/>
        <env name="DB_SECOND_DRIVER" value="sqlite"/>
        <env name="DB_SECOND_DATABASE" value=":memory:"/>
    </php>
</phpunit>

This ensures that Laravel correctly picks up the database configuration during testing.

@otlnrs
Copy link

otlnrs commented Dec 11, 2024

I think the issue with using the new config options from @stancl that @crynobone suggested is that they're incompatible with usingInMemoryDatabase() in Illuminate\Foundation\Testing\RefreshDatabase.php, because it will always return false if used with the default database, and isn't flexible enough to be used with a secondary DB:

protected function usingInMemoryDatabase()  
{  
    $default = config('database.default');  
    return config("database.connections.$default.database") === ':memory:';  
}

@stancl
Copy link
Contributor

stancl commented Dec 11, 2024

That's a method on a class you control as my PR mentions. Try overriding it to return:

protected function usingInMemoryDatabase()
{
    $default = config('database.default');
    $database = config("database.connections.$default.database");

    return $database === ':memory:' || str_contains($database, 'mode=memory');
}

The PR adding the feature in is slim and focused on core Laravel logic rather than helper traits that can be easily modified by the user. If such overrides work well and people use them often the change can be submitted in another PR.

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

No branches or pull requests

6 participants