From debe87d23b1cefd8f0edbf5e3f495f18e9d9fa58 Mon Sep 17 00:00:00 2001 From: Patrick O'Meara Date: Fri, 13 Jan 2023 16:35:11 +1100 Subject: [PATCH 1/7] Add DatabaseTruncates option for setting up DB Migrate the database in the first run, then truncate the affected tables and use the seeder for subsequent runs. --- .../Foundation/Testing/DatabaseTruncates.php | 44 +++++++++++++++++++ .../Foundation/Testing/TestCase.php | 4 ++ .../Testing/Concerns/TestDatabases.php | 1 + 3 files changed, 49 insertions(+) create mode 100644 src/Illuminate/Foundation/Testing/DatabaseTruncates.php diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php new file mode 100644 index 000000000000..318f884a96a1 --- /dev/null +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php @@ -0,0 +1,44 @@ +beforeApplicationDestroyed(function () { + Schema::disableForeignKeyConstraints(); + collect(static::$allTables ??= DB::connection()->getDoctrineSchemaManager()->listTableNames()) + ->diff($this->excludeTables()) + ->filter(fn ($table) => DB::table($table)->exists()) + ->each(fn ($table) => DB::table($table)->truncate()); + }); + + // Migrate and seed the database on first run. + if (! RefreshDatabaseState::$migrated) { + $this->artisan('migrate:fresh', $this->migrateFreshUsing()); + RefreshDatabaseState::$migrated = true; + + return; + } + + // Seed the database on subsequent runs. + $this->artisan('db:seed', ['--class' => $this->seeder()]); + } + + protected function excludeTables() + { + return [ + 'migrations', + ]; + } +} diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index a782aeb79f9a..7c7bf4fb01c8 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -131,6 +131,10 @@ protected function setUpTraits() $this->runDatabaseMigrations(); } + if (isset($uses[DatabaseTruncates::class])) { + $this->truncateTables(); + } + if (isset($uses[DatabaseTransactions::class])) { $this->beginDatabaseTransaction(); } diff --git a/src/Illuminate/Testing/Concerns/TestDatabases.php b/src/Illuminate/Testing/Concerns/TestDatabases.php index eff8f276ddd4..515032f0b8d2 100644 --- a/src/Illuminate/Testing/Concerns/TestDatabases.php +++ b/src/Illuminate/Testing/Concerns/TestDatabases.php @@ -42,6 +42,7 @@ protected function bootTestDatabase() $databaseTraits = [ Testing\DatabaseMigrations::class, Testing\DatabaseTransactions::class, + Testing\DatabaseTruncates::class, Testing\RefreshDatabase::class, ]; From 42db879345e9d6d4c8cc7c307cc3eb503392a4ec Mon Sep 17 00:00:00 2001 From: Patrick O'Meara Date: Tue, 17 Jan 2023 07:29:46 +1100 Subject: [PATCH 2/7] Use a specific seeder if it's set, otherwise use the default --- .../Foundation/Testing/DatabaseTruncates.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php index 318f884a96a1..0f43ab249e60 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php @@ -32,7 +32,17 @@ protected function truncateTables() } // Seed the database on subsequent runs. - $this->artisan('db:seed', ['--class' => $this->seeder()]); + if ($seeder = $this->seeder()) { + // Use a specific seeder class. + $this->artisan('db:seed', ['--class' => $seeder]); + + return; + } + + if ($this->shouldSeed()) { + // Use the default seeder class. + $this->artisan('db:seed'); + } } protected function excludeTables() From 2b9a1b638e9b7d6ae66d261c76f81232e2bde3ec Mon Sep 17 00:00:00 2001 From: Patrick O'Meara Date: Tue, 17 Jan 2023 20:52:15 +1100 Subject: [PATCH 3/7] use migrations table from config --- src/Illuminate/Foundation/Testing/DatabaseTruncates.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php index 0f43ab249e60..b516e3c9d516 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php @@ -48,7 +48,7 @@ protected function truncateTables() protected function excludeTables() { return [ - 'migrations', + $this->app['config']->get('database.migrations'), ]; } } From b4b210228b80e655dfb28e007b09820bfaf6e2a4 Mon Sep 17 00:00:00 2001 From: Patrick O'Meara Date: Fri, 20 Jan 2023 08:16:15 +1100 Subject: [PATCH 4/7] Handle truncating multiple connections * allow excluding tables per connection * unset the event dispatcher before truncating, re-set afterwards --- .../Foundation/Testing/DatabaseTruncates.php | 63 ++++++++++++++----- .../Foundation/Testing/TestCase.php | 2 +- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php index b516e3c9d516..3ec13b7ed0a4 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php @@ -2,9 +2,9 @@ namespace Illuminate\Foundation\Testing; +use Illuminate\Contracts\Console\Kernel; +use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Schema; trait DatabaseTruncates { @@ -12,20 +12,17 @@ trait DatabaseTruncates protected static array $allTables; - protected function truncateTables() + protected function runDatabaseTruncates(): void { // Always remove any test data before the application is destroyed. - $this->beforeApplicationDestroyed(function () { - Schema::disableForeignKeyConstraints(); - collect(static::$allTables ??= DB::connection()->getDoctrineSchemaManager()->listTableNames()) - ->diff($this->excludeTables()) - ->filter(fn ($table) => DB::table($table)->exists()) - ->each(fn ($table) => DB::table($table)->truncate()); - }); + $this->beforeApplicationDestroyed(fn () => $this->truncateTables()); // Migrate and seed the database on first run. if (! RefreshDatabaseState::$migrated) { $this->artisan('migrate:fresh', $this->migrateFreshUsing()); + + $this->app[Kernel::class]->setArtisan(null); + RefreshDatabaseState::$migrated = true; return; @@ -45,10 +42,48 @@ protected function truncateTables() } } - protected function excludeTables() + protected function truncateTables(): void + { + /** @var \Illuminate\Database\DatabaseManager $database */ + $database = $this->app->make('db'); + + foreach ($this->connectionsToTruncate() as $name) { + $connection = $database->connection($name); + + $connection->getSchemaBuilder()->disableForeignKeyConstraints(); + + $dispatcher = $connection->getEventDispatcher(); + $connection->unsetEventDispatcher(); + + $this->truncateTablesForConnection($connection, $name); + + $connection->setEventDispatcher($dispatcher); + $connection->disconnect(); + } + } + + // Truncate all tables for a given connection. + protected function truncateTablesForConnection(ConnectionInterface $connection, ?string $name): void + { + collect(static::$allTables[$name] ??= $connection->getDoctrineSchemaManager()->listTableNames()) + ->diff($this->excludeTables($name)) + ->filter(fn ($table) => $connection->table($table)->exists()) + ->each(fn ($table) => $connection->table($table)->truncate()); + } + + // Get the tables that should not be truncated. + protected function excludeTables(?string $connectionName): array + { + return match ($connectionName) { + null => [$this->app['config']->get('database.migrations')], + default => [], + }; + } + + // The database connections that should be truncated. + protected function connectionsToTruncate(): array { - return [ - $this->app['config']->get('database.migrations'), - ]; + return property_exists($this, 'connectionsToTruncate') + ? $this->connectionsToTruncate : [null]; } } diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 7c7bf4fb01c8..54e09a65c331 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -132,7 +132,7 @@ protected function setUpTraits() } if (isset($uses[DatabaseTruncates::class])) { - $this->truncateTables(); + $this->runDatabaseTruncates(); } if (isset($uses[DatabaseTransactions::class])) { From 9d14265932abd6fc871aa0f08fc08ff67eb5a14b Mon Sep 17 00:00:00 2001 From: Patrick O'Meara Date: Fri, 20 Jan 2023 11:47:37 +1100 Subject: [PATCH 5/7] Get the database ready at the start of the test * This removes potential conflicts with other beforeApplicationDestroyed callbacks, where a database is needed * This matches the behaviour of the other tests instead of removing data afterwards. * It also makes it easier to check the data after failed tests, especially when a dusk test fails. --- src/Illuminate/Foundation/Testing/DatabaseTruncates.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php index 3ec13b7ed0a4..5bef673b8d87 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php @@ -14,9 +14,6 @@ trait DatabaseTruncates protected function runDatabaseTruncates(): void { - // Always remove any test data before the application is destroyed. - $this->beforeApplicationDestroyed(fn () => $this->truncateTables()); - // Migrate and seed the database on first run. if (! RefreshDatabaseState::$migrated) { $this->artisan('migrate:fresh', $this->migrateFreshUsing()); @@ -28,6 +25,9 @@ protected function runDatabaseTruncates(): void return; } + // Always remove any test data before running the tests. + $this->truncateTables(); + // Seed the database on subsequent runs. if ($seeder = $this->seeder()) { // Use a specific seeder class. From e0199be1d416d3dc79274004c500318b0f80ec10 Mon Sep 17 00:00:00 2001 From: Patrick O'Meara Date: Fri, 20 Jan 2023 12:35:06 +1100 Subject: [PATCH 6/7] useForeignKeyChecks * allow the developer to specify which connections use foreign key checks, default to all connections * allwo the developer to create an excludeTables property instead of having to override the method --- .../Foundation/Testing/DatabaseTruncates.php | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php index 5bef673b8d87..aa448a2fb088 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncates.php @@ -25,10 +25,9 @@ protected function runDatabaseTruncates(): void return; } - // Always remove any test data before running the tests. - $this->truncateTables(); + // Always clear any test data on subsequent runs. + $this->clearPreviousTestData(); - // Seed the database on subsequent runs. if ($seeder = $this->seeder()) { // Use a specific seeder class. $this->artisan('db:seed', ['--class' => $seeder]); @@ -42,42 +41,59 @@ protected function runDatabaseTruncates(): void } } - protected function truncateTables(): void + protected function clearPreviousTestData(): void { /** @var \Illuminate\Database\DatabaseManager $database */ $database = $this->app->make('db'); - foreach ($this->connectionsToTruncate() as $name) { - $connection = $database->connection($name); + collect($this->connectionsToTruncate()) + ->each(function ($name) use ($database) { + $connection = $database->connection($name); - $connection->getSchemaBuilder()->disableForeignKeyConstraints(); + if (! $this->useForeignKeyChecks($name)) { + $this->truncateTablesForConnection($connection, $name); - $dispatcher = $connection->getEventDispatcher(); - $connection->unsetEventDispatcher(); + return; + } - $this->truncateTablesForConnection($connection, $name); - - $connection->setEventDispatcher($dispatcher); - $connection->disconnect(); - } + $connection->getSchemaBuilder()->withoutForeignKeyConstraints( + fn () => $this->truncateTablesForConnection($connection, $name) + ); + }); } // Truncate all tables for a given connection. protected function truncateTablesForConnection(ConnectionInterface $connection, ?string $name): void { + $dispatcher = $connection->getEventDispatcher(); + $connection->unsetEventDispatcher(); + collect(static::$allTables[$name] ??= $connection->getDoctrineSchemaManager()->listTableNames()) ->diff($this->excludeTables($name)) ->filter(fn ($table) => $connection->table($table)->exists()) ->each(fn ($table) => $connection->table($table)->truncate()); + + $connection->setEventDispatcher($dispatcher); } // Get the tables that should not be truncated. protected function excludeTables(?string $connectionName): array { - return match ($connectionName) { - null => [$this->app['config']->get('database.migrations')], - default => [], - }; + if (property_exists($this, 'excludeTables')) { + return $this->excludeTables[$connectionName] ?? []; + } + + return [$this->app['config']->get('database.migrations')]; + } + + // Should foreign key checks be enabled after truncating tables? + protected function useForeignKeyChecks(?string $connectionName): bool + { + if (property_exists($this, 'useForeignKeyChecks')) { + return $this->useForeignKeyChecks[$connectionName] ?? true; + } + + return true; } // The database connections that should be truncated. From 8a75339c68f7dc783bdea9e6c1a26ce437af378a Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 31 Jan 2023 16:17:26 -0600 Subject: [PATCH 7/7] formatting --- ...seTruncates.php => DatabaseTruncation.php} | 95 +++++++++++-------- .../Foundation/Testing/TestCase.php | 4 +- .../Testing/Concerns/TestDatabases.php | 2 +- 3 files changed, 57 insertions(+), 44 deletions(-) rename src/Illuminate/Foundation/Testing/{DatabaseTruncates.php => DatabaseTruncation.php} (53%) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php similarity index 53% rename from src/Illuminate/Foundation/Testing/DatabaseTruncates.php rename to src/Illuminate/Foundation/Testing/DatabaseTruncation.php index aa448a2fb088..16141c0a55af 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncates.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php @@ -6,15 +6,25 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands; -trait DatabaseTruncates +trait DatabaseTruncation { use CanConfigureMigrationCommands; + /** + * The cached names of the database tables for each connection. + * + * @var array + */ protected static array $allTables; - protected function runDatabaseTruncates(): void + /** + * Truncate the database tables for all configured connections. + * + * @return void + */ + protected function truncateDatabaseTables(): void { - // Migrate and seed the database on first run. + // Migrate and seed the database on first run... if (! RefreshDatabaseState::$migrated) { $this->artisan('migrate:fresh', $this->migrateFreshUsing()); @@ -25,81 +35,84 @@ protected function runDatabaseTruncates(): void return; } - // Always clear any test data on subsequent runs. - $this->clearPreviousTestData(); + // Always clear any test data on subsequent runs... + $this->truncateTablesForAllConnections(); if ($seeder = $this->seeder()) { - // Use a specific seeder class. + // Use a specific seeder class... $this->artisan('db:seed', ['--class' => $seeder]); - - return; - } - - if ($this->shouldSeed()) { - // Use the default seeder class. + } elseif ($this->shouldSeed()) { + // Use the default seeder class... $this->artisan('db:seed'); } } - protected function clearPreviousTestData(): void + /** + * Truncate the database tables for all configured connections. + * + * @return void + */ + protected function truncateTablesForAllConnections(): void { - /** @var \Illuminate\Database\DatabaseManager $database */ $database = $this->app->make('db'); collect($this->connectionsToTruncate()) ->each(function ($name) use ($database) { $connection = $database->connection($name); - if (! $this->useForeignKeyChecks($name)) { - $this->truncateTablesForConnection($connection, $name); - - return; - } - $connection->getSchemaBuilder()->withoutForeignKeyConstraints( fn () => $this->truncateTablesForConnection($connection, $name) ); }); } - // Truncate all tables for a given connection. + /** + * Truncate the database tables for the given database connection. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param string|null $name + * @return void + */ protected function truncateTablesForConnection(ConnectionInterface $connection, ?string $name): void { $dispatcher = $connection->getEventDispatcher(); + $connection->unsetEventDispatcher(); collect(static::$allTables[$name] ??= $connection->getDoctrineSchemaManager()->listTableNames()) - ->diff($this->excludeTables($name)) + ->diff($this->exceptTables($name)) ->filter(fn ($table) => $connection->table($table)->exists()) ->each(fn ($table) => $connection->table($table)->truncate()); $connection->setEventDispatcher($dispatcher); } - // Get the tables that should not be truncated. - protected function excludeTables(?string $connectionName): array + /** + * The database connections that should have their tables truncated. + * + * @return array + */ + protected function connectionsToTruncate(): array { - if (property_exists($this, 'excludeTables')) { - return $this->excludeTables[$connectionName] ?? []; - } - - return [$this->app['config']->get('database.migrations')]; + return property_exists($this, 'connectionsToTruncate') + ? $this->connectionsToTruncate : [null]; } - // Should foreign key checks be enabled after truncating tables? - protected function useForeignKeyChecks(?string $connectionName): bool + /** + * Get the tables that should not be truncated. + * + * @param string|null $connectionName + * @return array + */ + protected function exceptTables(?string $connectionName): array { - if (property_exists($this, 'useForeignKeyChecks')) { - return $this->useForeignKeyChecks[$connectionName] ?? true; + if (property_exists($this, 'exceptTables')) { + return array_merge( + $this->exceptTables[$connectionName] ?? [], + [$this->app['config']->get('database.migrations')] + ); } - return true; - } - - // The database connections that should be truncated. - protected function connectionsToTruncate(): array - { - return property_exists($this, 'connectionsToTruncate') - ? $this->connectionsToTruncate : [null]; + return [$this->app['config']->get('database.migrations')]; } } diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 54e09a65c331..e7968631c107 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -131,8 +131,8 @@ protected function setUpTraits() $this->runDatabaseMigrations(); } - if (isset($uses[DatabaseTruncates::class])) { - $this->runDatabaseTruncates(); + if (isset($uses[DatabaseTruncation::class])) { + $this->truncateDatabaseTables(); } if (isset($uses[DatabaseTransactions::class])) { diff --git a/src/Illuminate/Testing/Concerns/TestDatabases.php b/src/Illuminate/Testing/Concerns/TestDatabases.php index 515032f0b8d2..f13ed053784f 100644 --- a/src/Illuminate/Testing/Concerns/TestDatabases.php +++ b/src/Illuminate/Testing/Concerns/TestDatabases.php @@ -42,7 +42,7 @@ protected function bootTestDatabase() $databaseTraits = [ Testing\DatabaseMigrations::class, Testing\DatabaseTransactions::class, - Testing\DatabaseTruncates::class, + Testing\DatabaseTruncation::class, Testing\RefreshDatabase::class, ];