Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel Testing Increasing Memory Use

Time: 1.89 minutes, Memory: 526.00MB

OK (487 tests, 2324 assertions)

This is my phpunittest result from testing my Laravel API, the memory consumption just keeps increasing and feel like i tried all posts and answers across the internet for what keeps racking up memory when testing. From own debugging the App is trashed every test which it should.

Everything is pretty standard, with the createApplication method that looks like this.

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;
}

Came to the conclusion that the memory leak is in, which does not get cleaned up properly.

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

$app->make(Kernel::class)->bootstrap();
like image 229
mrhn Avatar asked Aug 23 '16 08:08

mrhn


2 Answers

Here is the simplest and the most reliable solution I've found. It doesn't have the disadvantages described in my previous answer.

Change the <phpunit> tag processIsolation attribute value to true in the phpunit.xml file. An example of the start of a proper phpunit.xml file:

<?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">
...

How it works: it makes PhpUnit start a new PHP process for each test. Running tests in separate processes forces PHP to free all the memory after a test is over. It slows down the tests but it's the price for the low memory consumption and the reliability.

Or you can add a @runTestsInSeparateProcesses annotation to the doc block of a test class if you need to run in separate processes only certain tests:

/**
 * @runTestsInSeparateProcesses
 */
class HeavyTest extends TestCase
{
    // ...
}
like image 127
Finesse Avatar answered Nov 09 '22 22:11

Finesse


I struggled for this for a long time temporarily solving the issue by turning on process isolation. That kind of worked but was super-slow and caused other issues.

The high memory usage is because the Laravel application object contains many references to other objects. PHP isn't very good at garbage collecting objects with many interconnected references so the objects hang around causing massive memory usage.

I solved this by extending builtin Application object and adding my own clean up method that's called after every test:

Note: This is for Laravel 4.2 but the same principal should work just as well with Laravel 5+.

Add app\foundation\Application.php (this may differ depending on how you're using namespaces).

<?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);
    }

}

Ensure Laravel is autoloading the new class by editing the composer.json file and adding app/foundation to autoload > classmap. You may want to add the path to app/start/global.php (ClassLoader::addDirectories) too.

Edit bootstrap\start.php and change $app = new Illuminate\Foundation\Application; to $app = new App\Foundation\Application;.

Now edit your app/tests/TestCase.php and add your own tearDown method:

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

All this does is call the clean() method after every test essentially removing the references before the object is garbage collected.

like image 43
Tama Avatar answered Nov 09 '22 22:11

Tama