问题描述
我刚刚开始处理这个测试为零的大项目。这个想法是TDD每一个新功能和/或错误,随着时间的推移,我们将增加测试范围。
我不使用sqlite内存数据库进行测试。我确实更喜欢使用MysqL,因为它与我在生产中使用的数据库相同。通常,在小项目中,这没问题,但是在大项目中,是这样!
我遇到的问题与性能有关,一个正常的MysqL实例在磁盘上运行(M.2 SSD)大约需要90秒才能完成此大项目的所有迁移。有200多个表要迁移,并且具有很多关系。
此问题的解决方案是也将tmpfs
与docker一起在内存中设置MysqL。这个技巧使我将迁移时间减少到 10秒,还不错,但是如果您只想运行1个测试,那真是令人讨厌!迁移需要10秒,要测试几毫秒。
Laravel 8刚刚引入了一个名为Schema Dump
的新功能:https://github.com/laravel/framework/pull/32275
我刚刚看到了这个新功能,这真的使我很开心,非常好!这将帮助很多人并节省很多时间。如果您有很多迁移,则可以大大减少迁移所有这些文件的时间。 否则,这不能解决我的问题。该项目的迁移数量非常接近每个表1个迁移。无需在此处进行任何优化。
出于好奇,我对数据库进行了架构快照,并尝试使用MySQL命令行将其还原。花了 3秒来运行模式还原并设置所有内容:
MysqL -h 127.0.0.1 -u root -P 3331 -p default < database/migrations.sql
暂时,测试数据库一直保持迁移状态,这样我的测试流程(一次测试一次运行)就保持了超快的速度!
我想认为单个测试应该像您按下的按钮一样,并立即以绿色或红色点亮。
我的问题:-是否可以减少具有大量表的项目的迁移时间?(仅用于测试)
我没有有关MysqL的内在知识,也许我缺少一些东西...
解决方法
如果目标是在某个时间点加载数据库,而您需要重复使用同一快照,那么我建议您尝试使用LVM快照,而不是“迁移”。
它涉及磁盘的操作系统级快照。您可以将磁盘上仅包含MySQL数据集,并使用LVM来进行类似的操作:
一次设置:停止mysqld,拍摄LVM快照
准备重新加载该快照时,请执行其他LVM魔术操作以使用快照而不是磁盘的当前状态。
对不起,我无法预计将花费几秒钟,但它根本不涉及mysqldump。
,只是一个提示
当我们具有不同的测试功能时,我也遇到过这种情况,在每次测试后重置数据库通常很有用,这样以前测试的数据不会干扰后续测试。
我采用了以下方法,希望您能从中获得一些帮助,只是分享一些技巧。
如果一个大型项目中有40多个表,则从头开始重写和部署所有迁移可能不是理想的选择。在这种情况下,一种解决方案是将新迁移的数据库导出为SQL“快照”。与原始迁移相比,该包含所有迁移作为原始SQL查询的文件将大大加快解析和执行的速度。
如果您想测试站点的每个功能,那么Laravel's RefreshDatabase trait
就很有意义,实际上,它是用来从新迁移的数据库开始每个测试的。在后台,Laravel会在每次测试之前运行以下代码:
protected function refreshTestDatabase()
{
if (! RefreshDatabaseState::$migrated) {
$this->artisan('migrate:fresh');
RefreshDatabaseState::$migrated = true;
}
$this->beginDatabaseTransaction();
}
如您所见,迁移将仅在第一次测试之前运行一次。第一次测试后,数据库事务将用于快速恢复到初始迁移的数据库状态。在运行多个测试时,这可以节省大量时间。但是,运行这些初始迁移的时间会大大延迟您的测试结果。如果您只想运行一个“快速”测试,这会特别烦人
实施
首先清除数据库,然后使用php artisan migrate:fresh
运行迁移。然后打开您的首选数据库客户端并导出(或备份)空数据库。您应该只剩下一个SQL文件。让我们将该文件重命名为migrations_2019_01_10.sql
并将其放在应用程序的数据库目录中。
接下来,我们将不得不在RefreshDatabase trait
中执行此SQL文件。您可以将整个特征复制到自己的代码库中,也可以直接在自己的TestCase.php
中覆盖该方法。最终看起来像这样:
abstract class TestCase extends \Illuminate\Foundation\Testing\TestCase
{
use CreatesApplication;
use RefreshDatabase;
protected function refreshTestDatabase()
{
if (! RefreshDatabaseState::$migrated) {
DB::unprepared(file_get_contents(database_path('migrations_2019_01_10.sql')));
$this->artisan('migrate');
$this->app[Kernel::class]->setArtisan(null);
RefreshDatabaseState::$migrated = true;
}
$this->beginDatabaseTransaction();
}
}
如您所见,我将migrate
命令保留在其中,以运行可能在我们的migrations.sql
快照之后添加的所有新迁移。这样,您无需在每次添加迁移时都导出已迁移的数据库。只要记住不时准备一次新快照即可。
感谢saddam kamal和shock_gone_wild触发了一个想法,使我为这个问题清除了思路。我不需要每次运行测试都迁移数据库。在当前的工作流程中,我每天手动迁移所有内容。这可以自动化!
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
// first run of the day,// the database will be migrated to tmpfs
$result = DB::select(DB::raw("SHOW TABLES LIKE 'users';"));
if (!count($result))
{
$this->artisan('migrate:fresh');
}
}
}
我不知道我在想什么!这确实是一段简单的代码,它使所有事情都实现了。由于数据库位于内存(tmpfs)中,因此在一天的第一次测试中只需要迁移一次。第一次运行大约需要10秒钟,下次测试将以毫秒为单位。
,我使用的方法在初次设置测试时可能会感到笨拙,但对运行测试套件的速度有很大的影响。仅运行该测试所需的那些迁移,而不是运行所有迁移或执行完整的架构导入。例如:
在测试中:
$this->migrate([
'2016_04_29_132815_create_authors_table','2016_04_29_132815_create_categories_table'
]);
$this->seed(CategoriesTableSeeder::class);
这在TestCase中:
use Artisan;
/**
* Runs migrations for individual tests
*
* @params array $migrations
* @return void
*/
public function migrate(array $migrations = []): void
{
$path = database_path('migrations');
$migrator = app()->make('migrator');
$migrator->getRepository()->createRepository();
$files = $migrator->getMigrationFiles($path);
if (!empty($migrations)) {
$files = collect($files)->filter(
function ($value,$key) use ($migrations) {
if (in_array($key,$migrations)) {
return [$key => $value];
}
}
)->all();
}
$migrator->requireFiles($files);
$migrator->runPending($files);
}
/**
* Runs some or all seeds
*
* @params string $seeds
* @return void
*/
public function seed($seeds = ''): void
{
$command = "db:seed";
if (empty($seeds)) {
Artisan::call($command);
} else {
Artisan::call($command,['--class' => $seeds]);
}
}