Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid Serialization of Closure with process isolation on PHPUnit?

I am trying to write an integration test for an extension (app) for nextcloud. Nextcloud itself is based on Symfony. Long story short, I ended so far with a test class, that throws the following error message:

PHPUnit 8.5.15 by Sebastian Bergmann and contributors.

IE                                                                  2 / 2 (100%)

Time: 642 ms, Memory: 24.00 MB

There was 1 error:

1) tests\Integration\Setup\Migrations\Version000000Date20210701093123Test::testRedundantEntriesInDB with data set "caseC" (array(), array('bob'))
PHPUnit\Framework\Exception: PHP Fatal error:  Uncaught Exception: Serialization of 'Closure' is not allowed in Standard input code:340
Stack trace:
#0 Standard input code(340): serialize(Array)
#1 Standard input code(1237): __phpunit_run_isolated_test()
#2 {main}
  thrown in Standard input code on line 340

ERRORS!
Tests: 2, Assertions: 1, Errors: 1, Incomplete: 1.

I tried to find the culprit for this error and as far as I can understand this is due to the fact that I am using @runInSeparateProcess as an annotation to the named test and some global state seems to be existing that PHPUnit tries to save/serialize for the child PHP process.

The simplified MWE code is available in a branch on github or below for later reference. I tried to narrow down the problem after reading Symfony 2 + Doctrine 2 + PHPUnit 3.5: Serialization of closure exception and similar questions here. The created container is some sort of global storage that will hold all kinds of instances for the dependency injection method.

How can I avoid that PHPUnit tries to preserve this state? In fact, I use the process separation to have a clean starting environment for the tests.

What surprises me a bit is that the error only manifests when the second function parameter from the data provider is a non-empty array.

If required, it is possible to clone the repo and build some docker containers (under Linux, IDK about Windows) to run the test manually. Just go to .github/actions/run-tests and call ./run-locally.sh --prepare stable21 to build the nvironment (take a big cup of coffee). Then the test can be started with ./run-locally.sh --run-integration-tests --filter 'tests\\Integration\\Setup\\Migrations\\Version000000Date20210701093123Test'.


Here comes the basic MWE code:

<?php

namespace tests\Integration\Setup\Migrations;

use OCP\AppFramework\App;
use OCP\AppFramework\IAppContainer;
use PHPUnit\Framework\TestCase;

class Version000000Date20210701093123Test extends TestCase {
    
    /**
     * @var IAppContainer
     */
    private $container;
    
    public function setUp(): void {
        parent::setUp();
        
        $app = new App('cookbook');
        $this->container = $app->getContainer();
    }
    
    /**
     * @dataProvider dataProvider
     * @runInSeparateProcess
     */
    public function testRedundantEntriesInDB($data, $updatedUsers) {
//         print_r($updatedUsers);
        sort($updatedUsers);
//         print_r($updatedUsers);
        
        $this->assertEquals($updatedUsers, []);
        
        $this->markTestIncomplete('Not yet implemented');
    }
    
    public function dataProvider() {
        return [
            'caseB' => [
                [
                ],
                [],
            ],
            'caseC' => [
                [
                ],
                ['bob']
            ],
        ];
    }
}
like image 558
Christian Wolf Avatar asked Sep 01 '25 17:09

Christian Wolf


1 Answers

When the test-runner creates the plan to run the tests, it collects which test-methods with which data-sets need to be executed.

To do this, the data-set is obtained.

Now for the test-method the process isolation is demanded (@runInSeparateProcess). Therefore the test-method is called in a PHP process of its own and the record from the data-set to call it with is passed to it.

As this is a separate process, the data-set is serialized (marshalled) so that it can be unserialized (unmarshalled) in the separate test-process where the test is run.

If the data-set contains data that refuses to serialize, the test can not be executed.

Similar, if the test executes but the result contains data that can not be serialized (so that it can be passed back to the main runner process), the test as well fails.

The later is likely your case.

Not being able to serialize the data is a fatal error in PHP which means that the separate php process exits in error hard. As it is a separate process it can be caught by phpunit runner, but the test is marked as error.


  • Add a tearDown method to the test-case.
  • Unset the $this->container in tear-down.
  • Try again.
  • (might be n/a: Symfony is also known to run on caches of all sorts, inspecting $GLOBALS might reveal some insights on how to properly kill the App if it refuses to go away)
like image 199
hakre Avatar answered Sep 04 '25 07:09

hakre