【问题标题】:How to Migrate and seed before the full test suite in Laravel with in memory database?如何在 Laravel 中使用内存数据库的完整测试套件之前迁移和播种?
【发布时间】: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 加载固定装置。

【问题讨论】:

    标签: php laravel phpunit laravel-5.2


    【解决方案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/seedinglaravel.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 秒
        猜你喜欢
        • 2016-12-30
        • 2017-07-10
        • 2020-06-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-03
        • 1970-01-01
        • 2022-01-24
        相关资源
        最近更新 更多