Laravel 测试增加内存使用

Laravel Testing Increasing Memory Use

Time: 1.89 minutes, Memory: 526.00MB

OK (487 tests, 2324 assertions)

这是我测试我的 Laravel API 的 phpunittest 结果,内存消耗一直在增加,感觉我尝试了互联网上的所有帖子和答案,以了解在测试时不断增加内存的原因.通过自己的调试,应用程序在每次测试时都被丢弃了。

一切都非常标准,createApplication 方法如下所示。

public function createApplication()
{
    // Ran out of memory
    ini_set('memory_limit', '1024M');

    $app = require __DIR__ . '/../bootstrap/app.php';

    $app->make(Kernel::class)->bootstrap();

    return $app;
}

得出的结论是存在内存泄漏,但未正确清理。

$app = require __DIR__ . '/../bootstrap/app.php';

$app->make(Kernel::class)->bootstrap();

为此纠结了很久,暂时通过开启进程隔离解决了这个问题。这种方法有效,但速度超慢并导致其他问题。

内存使用率高是因为Laravel 应用程序对象包含许多对其他对象的引用。 PHP 不擅长对具有许多相互关联的引用的对象进行垃圾回收,因此对象会四处闲逛,导致大量内存使用。

我通过扩展内置应用程序对象并添加我自己的每次测试后调用的清理方法解决了这个问题:

注意:这是针对 Laravel 4.2 的,但相同的主体应该与 Laravel 5+ 一样适用。

添加 app\foundation\Application.php(这可能会有所不同,具体取决于您使用命名空间的方式)。

<?php

namespace App\Foundation;

class Application extends \Illuminate\Foundation\Application
{
    /**
     * Used during unit tests to clear out all the data structures
     * that may contain references.
     * Without this phpunit runs out of memory.
     */
    public function clean()
    {
        unset($this->bootingCallbacks);
        unset($this->bootedCallbacks);
        unset($this->finishCallbacks);
        unset($this->shutdownCallbacks);
        unset($this->middlewares);
        unset($this->serviceProviders);
        unset($this->loadedProviders);
        unset($this->deferredServices);
        unset($this->bindings);
        unset($this->instances);
        unset($this->reboundCallbacks);

        unset($this->resolved);
        unset($this->aliases);
    }

}

通过编辑 composer.json 文件并将 app/foundation 添加到 autoload > classmap,确保 Laravel 自动加载新的 class。您可能还想将路径添加到 app/start/global.php (ClassLoader::addDirectories)。

编辑 bootstrap\start.php 并将 $app = new Illuminate\Foundation\Application; 更改为 $app = new App\Foundation\Application;

现在编辑您的 app/tests/TestCase.php 并添加您自己的 tearDown 方法:

public function tearDown()
{
    $this->app->clean();
    unset($this->app);
    unset($this->client);
    parent::tearDown();
}

所有这一切都是在每次测试之后调用 clean() 方法,基本上在对象被垃圾收集之前删除引用。

这是一个简单的解决方案。我在 Laravel 5.5 测试过它。将其设置为 tests/CreatesApplication.php 文件的内容:

namespace Tests;

use Illuminate\Contracts\Console\Kernel;

trait CreatesApplication
{
    /**
     * @var \Illuminate\Foundation\Application
     */
    protected static $application;

    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        if (!isset(static::$application)) {
            static::$application = require __DIR__ . '/../bootstrap/app.php';
            static::$application->make(Kernel::class)->bootstrap();
        }

        return static::$application;
    }

    /**
     * {@inheritDoc}
     */
    protected function tearDown()
    {
        // Required if you use HTTP tests
        $this->app['auth']->guard(null)->logout();
        $this->app['auth']->shouldUse(null);
        $this->flushSession();

        // Always required
        $this->app = null;
        parent::tearDown();
    }
}

此代码使测试对所有测试使用相同的 Laravel 应用程序实例,而不是为每个测试创建一个新实例。这也会导致更快的测试 运行.

此解决方案有缺点:

  • 如果一个测试修改了应用程序,其他测试可能会失败,因为它们期望一个未更改的应用程序。
  • 如果测试使用 DatabaseTransactionsDatabaseMigrations 特征,当 PHPUnit 测试 运行 的所有测试都完成时,数据库将被重置。

这是我找到的最简单、最可靠的解决方案。它没有 .

中描述的缺点

phpunit.xml 文件中将 <phpunit> 标签 processIsolation 属性值更改为 true。正确 phpunit.xml 文件开头的示例:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="true"
         stopOnFailure="false">
...

工作原理:它使 PhpUnit 为每个测试启动一个新的 PHP 进程。 运行 在单独的进程中进行测试会强制 PHP 在测试结束后释放所有内存。它减慢了测试速度,但这是低内存消耗和可靠性的代价。

或者如果您需要 运行 在单独的进程中仅某些测试,您可以将 @runTestsInSeparateProcesses annotation 添加到测试 class 的文档块中:

/**
 * @runTestsInSeparateProcesses
 */
class HeavyTest extends TestCase
{
    // ...
}

我遇到了类似的问题,但这是由于在单元测试期间启用了 Telescope。将以下内容添加到 phpunit.xml 后,我的内存使用量减少了大约 75%。

<server name="TELESCOPE_ENABLED" value="false"/>