如何在 Laravel 中使用内存数据库的完整测试套件之前迁移和播种?

Posted

技术标签:

【中文标题】如何在 Laravel 中使用内存数据库的完整测试套件之前迁移和播种?【英文标题】:How to Migrate and seed before the full test suite in Laravel with in memory database? 【发布时间】:2016-11-29 03:42:21 【问题描述】:

我正在尝试在我的 Laravel 项目中设置测试环境。 我正在使用带有 json 的http://packalyst.com/packages/package/mayconbordin/l5-fixtures 在内存数据库中使用 sqlite 进行播种并调用:

Artisan::call('migrate');
Artisan::call('db:seed');

在我的 setUp 函数中,但这是在每个测试之前执行的,在这个项目中它可以增长到数千个。

我尝试了 setUpBeforeClass,但没有成功。 我认为是因为在每个测试中都会调用 createApplication 方法,并且重置了整个应用程序,并且可能出于同样的原因也没有从 json 加载固定装置。

【问题讨论】:

【参考方案1】:

如果其他人遇到同样的问题,我就是这样做的,我创建了一个基类 testClase 继承自 Laravel 的类并这样做了:

/**
 * Creates the application.
 *
 * @return \Illuminate\Foundation\Application
 */
public function createApplication()

    return self::initialize();


private static $configurationApp = null;
public static function initialize()

    if(is_null(self::$configurationApp))
        $app = require __DIR__.'/../bootstrap/app.php';

        $app->loadEnvironmentFrom('.env.testing');

        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

        if (config('database.default') == 'sqlite') 
            $db = app()->make('db');
            $db->connection()->getPdo()->exec("pragma foreign_keys=1");
        

        Artisan::call('migrate');
        Artisan::call('db:seed');

        self::$configurationApp = $app;
        return $app;
    

    return self::$configurationApp;


public function tearDown()

    if ($this->app) 
        foreach ($this->beforeApplicationDestroyedCallbacks as $callback) 
            call_user_func($callback);
        

    

    $this->setUpHasRun = false;

    if (property_exists($this, 'serverVariables')) 
        $this->serverVariables = [];
    

    if (class_exists('Mockery')) 
        Mockery::close();
    

    $this->afterApplicationCreatedCallbacks = [];
    $this->beforeApplicationDestroyedCallbacks = [];

我重写了 createApplication()tearDown() 方法。我将第一个更改为使用相同的$app 配置,并删除了teardown() 中它刷新$this->app 的部分。

我的所有其他测试都必须从这个 TestClass 继承,仅此而已。

其他一切都不起作用。这甚至适用于内存数据库,速度提高了 100 倍。

如果您正在处理用户会话,一旦您登录用户,您将不得不将他注销,否则用户将登录,因为应用程序环境永远不会重建,或者您可以执行类似的操作随时刷新应用程序:

protected static $applicationRefreshed = false;

/**
 * Refresh the application instance.
 *
 * @return void
 */
protected function forceRefreshApplication() 
    if (!is_null($this->app)) 
        $this->app->flush();
    
    $this->app = null;
    self::$configurationApp = null;
    self::$applicationRefreshed = true;
    parent::refreshApplication();

并将其添加到tearDown() 之前的$this->setUphasRun = false;

if (self::$applicationRefreshed) 
        self::$applicationRefreshed = false;
        $this->app->flush();
        $this->app = null;
        self::$configurationApp = null;

【讨论】:

太棒了!感谢您提供此解决方案)只有一个问题:如果我想强制刷新应用程序,那么迁移和播种将再次运行? Yes melihovv: if(is_null(self::$configurationApp))..... in the initialize() 函数中的所有内容只会在强制刷新应用程序时运行,在这种情况下我是在那里调用迁移和种子。 过去 24 小时我一直在寻找这个解决方案。我花了至少 14 个小时来寻找这个解决方案。非常感谢。我希望我可以请你一杯咖啡。 :) 我很惊讶在 Laravel 中在测试套件开始时刷新/播种数据库并不容易。但是laravel.com/docs/5.6/seeding 和laravel.com/docs/5.6/… 并没有达到我的预期。【参考方案2】:

就我而言,我创建了从.env.example 文件复制的.env.testing 文件。然后,我像这样将数据库信息添加到这个文件中。

APP_ENV=testing
APP_KEY=<generate your app key>
...
DB_CONNECTION=sqlite
DB_DATABASE=:memory:

在终端中,您可以像这样运行带有选项--env 的迁移工匠命令。

php artisan migrate:fresh --env=testing

【讨论】:

【参考方案3】:

在您的项目testrunner 中创建包含此内容的文件(同时准备.env.testing 包含测试环境变量的文件):

php artisan migrate:rollback --env=testing
php artisan migrate --env=testing --seed
vendor/bin/phpunit

并通过命令chmod +x testrunner授予执行权限,并通过./testrunner执行。就是这样:)

【讨论】:

这不会在测试环境下运行迁移命令(不同的数据库,内存中的sqlite)。 所以为了测试,可能将您的数据库更改为 mysql 真实环境有一个mysql数据库,我们想在内存中使用进行测试,至少目前是这样的计划。 您可以添加带有一些数据库查询的 if 语句,这将检测命令迁移和种子是否运行。如果没有,那么您将运行Artisan::call('migrate');... 不起作用,显然 Laravel 在拆除测试时破坏了整个环境。我在将 $this->app 设置为 null 并刷新它时评论了这些行,但它也不起作用。我猜这至少在内存数据库中将是一个死胡同。我会尝试更多的东西,看看它是否有效。【参考方案4】:

上述解决方案中的主要方法是为所有测试运行所有迁移。我更喜欢一种方法来指定每个测试应该运行哪些迁移和种子。

在大型项目上可能更值得,因为这可以将测试时间减少约 70%(使用上面已经解释过的 sqlite 内存数据库)。对于小型项目,这可能有点太花哨了。不过不管怎样……

在 TestCase 中使用这些:

/**
 * Runs migrations for individual tests
 *
 * @param array $migrations
 * @return void
 */
public function migrate(array $migrations = [])

    $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
 *
 * @param string $seed
 * @return void
 */
public function seed(string $seed = '')

    $command = "db:seed";

    if (empty($seed)) 
        Artisan::call($command);
     else 
        Artisan::call($command, ['--class' => $seed]);
    

然后在个别测试中根据需要调用 migrate() 和 seed,例如:

    $this->migrate(
        [
            '2013_10_11_081829_create_users_table',
        ]
    );
    $this->seed(UserTableSeeder::class);

【讨论】:

【参考方案5】:

选项 1

如何使用迁移和种子设置数据库,然后使用数据库事务? (https://laravel.com/docs/5.1/testing#resetting-the-database-after-each-test)

我希望能够像这样通过工匠设置我的测试数据库:

$ php artisan migrate --database=mysql_testing
$ php artisan db:seed --database=mysql_testing

你可以猜到,我使用的是 mysql,但我不明白为什么这不适用于 sqlite。 我就是这样做的。

config/database.php

首先将测试数据库信息添加到您的 config/database.php 文件中,在您当前的数据库信息下。

'connections' => [
        'mysql' => [
            'driver'    => 'mysql',
            'host'      => env('DB_HOST', 'localhost'),
            'database'  => env('DB_DATABASE', 'forge'),
            'username'  => env('DB_USERNAME', 'forge'),
            'password'  => env('DB_PASSWORD', ''),
            'charset'   => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix'    => '',
            'strict'    => false,
        ],
        'mysql_testing' => [
            'driver'    => 'mysql',
            'host'      => env('DB_HOST', 'localhost'),
            'database'  => env('DB_TEST_DATABASE'),
            'username'  => env('DB_USERNAME', 'forge'),
            'password'  => env('DB_PASSWORD', ''),
            'charset'   => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix'    => '',
            'strict'    => false,
        ],
    ],

如果您这样做,请不要忘记将 DB_TEST_DATABASE 添加到您的 .env 文件中:

DB_DATABASE=abc
DB_TEST_DATABASE=abc_test

phpunit.xml

phpunit.xml 文件中设置的任何值,覆盖 .env 文件中给出的值。所以我们告诉phpunit使用“mysql_testing”数据库连接而不是“mysql”数据库连接。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
    ...
    <php>
        ...
        <env name="DB_CONNECTION" value="mysql_testing"/>
</php>

测试类

我的测试类如下所示:

class MyTest extends \TestCase

    use \Illuminate\Foundation\Testing\DatabaseTransactions;

    public function testSomething()
    

选项 2

这里的数据库在每次测试之前都会重置,这就是我更喜欢选项 1 的原因。但你也许可以让它以你喜欢的方式工作。

我之前尝试过一次,它可能对你有用。

测试/TestCase.php 扩展测试用例,加载一个新的 .env 文件,.env.testing

<?php

class TestCase extends Illuminate\Foundation\Testing\TestCase

    /**
     * The base URL to use while testing the application.
     *
     * @var string
     */
    protected $baseUrl = 'http://localhost';

    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    
        /** @var $app \Illuminate\Foundation\Application */
        $app = require __DIR__.'/../bootstrap/app.php';
        $app->loadEnvironmentFrom('.env.testing');

        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

        return $app;
    

.env.testing

创建这个新的 .env 文件并添加数据库详细信息

APP_ENV=testing
APP_DEBUG=true
APP_KEY=xxx

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=abc_testing
DB_USERNAME=xxx
DB_PASSWORD=xxx

测试类中:

使用 PDO 删除并重新创建数据库 - 比尝试截断所有内容更容易。 然后使用 artisan 迁移和播种数据库。

class MyTest extends TestCase

    public static function setUpBeforeClass()
    
        $config = parse_ini_file(".env.testing");
        $username = $config['DB_USERNAME'];
        $password = $config['DB_PASSWORD'];
        $database = $config['DB_DATABASE'];
        $host = $config['DB_HOST'];

        // Create test database
        $connection = new PDO("mysql:host=$host", $username, $password);
        $connection->query("DROP DATABASE IF EXISTS " . $database);
        $connection->query("CREATE DATABASE " . $database);
    

    public function testHomePage()
    
        Artisan::call('migrate');
        Artisan::call('db:seed');

        $this->visit('/')
             ->see('Home')
             ->see('Please sign in')
             ->dontSee('Logout');
    

【讨论】:

我猜这将与 mysql 一起使用,但我使用的是内存数据库 o 它需要在测试运行时迁移/构建和播种,我只想为每个执行一次测试。还有其他建议吗? 好的,我用第二个选项更新了我的答案。它会在每次通话后重置数据库,而不是每次上课,但它可能会对您有所帮助。祝你好运:) 我需要 Artisan::call('migrate');工匠::call('db:seed');整个 testSuite 只执行一次,而不是每个测试。有什么想法吗? 这行得通。我已经尝试过第二个选项,但是测试太慢了,太慢了:6 分钟而不是 0.006 秒

以上是关于如何在 Laravel 中使用内存数据库的完整测试套件之前迁移和播种?的主要内容,如果未能解决你的问题,请参考以下文章

在 Laravel PHPUnit 测试中使用内存中的 SQLite

如何在不安装完整框架的情况下测试自定义 Laravel 控制台命令?

为啥 Laravel PHPUnit 内存测试会播种我的主数据库?

如何在 Laravel/Eloquent 中获得完整的关系

在laravel 5.2中使用工厂关系违反完整性约束

在 laravel 5.2 中使用工厂关系违反完整性约束