3

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']
            ],
        ];
    }
}
Christian Wolf
  • 1,187
  • 1
  • 12
  • 33

1 Answers1

1

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)
hakre
  • 193,403
  • 52
  • 435
  • 836
  • Both parameters to as well as from the separate process should be only arrays of strings. I checked so by commenting out the assertion (which allows the test to run smoothly) and printing the `gettype()` of the variables returning. Further, if you look closely, in my MWE I am just putting an assertion on the input variable. No output is generated in the test. -> The transport from the data provider to the separate process causes the trouble. – Christian Wolf Jul 14 '21 at 14:45
  • I can understand how it feels, but please keep in mind that a fatal error is normally not lying to you ;) My guess is its on passing back. That includes a large fraction of the test-runner and it may be connected to some parts that are not directly visible. Perhaps with xdebug 3 thanks to configuration by environment you can have step debugging incl. error breakpoints for better insights in that isolated PHP process as well. – hakre Jul 14 '21 at 14:57
  • But for step debugging, I need a source as well (apart from the fact that debugging in a subprocess of a process of a docker container... you get it). Because the PHP code executed is transmitted via pipe as stdin/stdout pair from the main PHPUnit process to the PHP subprocess, it will get hard to dig in there. Or am I misled? – Christian Wolf Jul 14 '21 at 15:08
  • that's a good point. your IDE will not find the sources, but the stack incl. variables should be available for what its worth. it would be something anyway in the direction that you really want to dig it. --- this sounds like a huge fixture. can you shutdown the system under test so that when the test is done you cleaned up as good as possible? Also you can try to isolate, e.g. single test method, one data-set and then run it normally, make your inspections etc.. --- kill the container in `::tearDown` , it is assigned to the testcase instance (e.g. `unset()`). – hakre Jul 14 '21 at 15:24
  • I tried a few things now. Unfortunately, the global state seems to be located in some static class variables. So no indication in `$GLOBALS` is found. I will have to dig into the debugging thing if this will work. I am unsure if my IDE (eclipse) will allow inspecting a debugging session without source code. – Christian Wolf Jul 18 '21 at 11:12
  • PS: For sake of completeness: the corresponding superglobal is called `$GLOBALS` not `$_GLOBALS`. I wanted to remove the typo fom the answer but I cannot unless I change 6 other words... – Christian Wolf Jul 18 '21 at 11:14
  • 1
    @ChristianWolf: Yes, true for _G..., fixed. - there is also [a command line debug client](https://xdebug.org/docs/dbgpClient) which may yield faster results. – hakre Jul 18 '21 at 12:26