Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Doubled methods cannot be called from destructor on test double created by the return value generator #5874

Open
frenchcomp opened this issue Jun 19, 2024 · 14 comments
Labels
feature/test-doubles Stubs and Mock Objects type/bug Something is broken version/11 Something affects PHPUnit 11

Comments

@frenchcomp
Copy link

Q A
PHPUnit version 11.2+
PHP version 8.2+
Installation Method Composer

Summary

Hi,
Since PHPUnit 11.2, I have issue when I want mock Symfony Process. I have no issue with PHPUnit 11.1 and priors versions.

Current behavior

When a mock the Symfony Process class, when I call a method on an instance of this mock, I have the error :
_Typed property MockObject_Process_XXXX::$_phpunit_state must not be accessed before initialization
I had test with define an expects() on the mock, but I had always the error.

There was 1 error:

  1. Test\IssueTest::testBug
    Error: Typed property MockObject_Process_62f16cad::$__phpunit_state must not be accessed before initialization

How to reproduce

I have reproducted the issue in this repository https://github.com/frenchcomp/test_phpunit_11_2_process
You can run the test case in the src folder : vendor/bin/phpunit src/IssueTest.php

@frenchcomp frenchcomp added the type/bug Something is broken label Jun 19, 2024
@sebastianbergmann
Copy link
Owner

Are you using the latest version of PHPUnit 11.2?

@sebastianbergmann sebastianbergmann added feature/test-doubles Stubs and Mock Objects version/11 Something affects PHPUnit 11 labels Jun 19, 2024
@sebastianbergmann
Copy link
Owner

I am sorry to say, but https://github.com/frenchcomp/test_phpunit_11_2_process is not a minimal, self-contained, reproducing test case.

Without such a minimal, self-contained, reproducing test case I will not be able to investigate this issue.

@sebastianbergmann sebastianbergmann added the status/waiting-for-feedback Waiting for feedback from original reporter label Jun 19, 2024
@frenchcomp
Copy link
Author

Yes :

$ composer show | grep phpunit
phpunit/php-code-coverage          11.0.3 Library that provides collection, processing, and rendering functionality for PHP code coverage information.
phpunit/php-file-iterator          5.0.0  FilterIterator implementation that filters files based on a list of suffixes.
phpunit/php-invoker                5.0.0  Invoke callables with a timeout
phpunit/php-text-template          4.0.0  Simple template engine.
phpunit/php-timer                  7.0.0  Utility class for timing
phpunit/phpunit                    11.2.3 The PHP Unit Testing framework.

@sebastianbergmann
Copy link
Owner

Are you using the latest version of PHPUnit 11.2?

Yes, you are (according to https://github.com/frenchcomp/test_phpunit_11_2_process/blob/main/composer.lock#L570).

@frenchcomp
Copy link
Author

I am sorry to say, but https://github.com/frenchcomp/test_phpunit_11_2_process is not a minimal, self-contained, reproducing test case.

Without such a minimal, self-contained, reproducing test case I will not be able to investigate this issue.

What is a "minimal, self-contained, reproducing test case" ?

@sebastianbergmann
Copy link
Owner

What is a "minimal, self-contained, reproducing test case" ?

Something that does not depend on a third-party library such as Symfony Process.

@sebastianbergmann sebastianbergmann changed the title I have the error "Typed property MockObject_Process_XXXX::$__phpunit_state must not be accessed before initialization" when I mock Symfony Process Typed property MockObject_Process_be57136f::$__phpunit_state must not be accessed before initialization error when doubling Symfony\Component\Process\Process Jun 19, 2024
@sebastianbergmann
Copy link
Owner

This is the full stack trace:

Error: Typed property MockObject_Process_be57136f::$__phpunit_state must not be accessed before initialization

/home/sb/test_phpunit_11_2_process/vendor/symfony/process/Process.php:215
/home/sb/test_phpunit_11_2_process/src/IssueTest.php:52

Process.php:215 is inside the destructor of Process.

@sebastianbergmann sebastianbergmann removed the status/waiting-for-feedback Waiting for feedback from original reporter label Jun 19, 2024
@frenchcomp
Copy link
Author

I updated my repo with a selfcontained class and removed process.
The issue is called when a method is called in the destructor of the doubled instance.

@sebastianbergmann
Copy link
Owner

sebastianbergmann commented Jun 19, 2024

In https://github.com/frenchcomp/test_phpunit_11_2_process/blob/18a41ac0d23c0d4f16483d9476ec0a7c77f3dbc5/src/IssueTest.php you have:

class IssueTest extends TestCase
{
    /**
     * @var Process
     */
    private $process;

    public function getProcessMock(): MockObject&Process
    {
        if (!$this->process instanceof Process) {
            $this->process = $this->createMock(Process::class);
        }

        return $this->process;
    }

    public function testBug()
    {
        $p = $this->getProcessMock();

        self::assertInstanceOf(
            Process::class,
            $p->setWorkingDirectory("foo"),
        );
    }
}

Here is what I understand to be happening:

  • After $p = $this->getProcessMock();, $p holds a reference to a mock object for type Process::class
  • For this mock object, all public, non-final methods of Process, except for __construct and __destruct have been replaced with implementations that can be configured using PHPUnit's API for test doubles
  • The $p->setWorkingDirectory("foo") method call is made on this mock object
  • Because no return value has been configured for the setWorkingDirectory method and because this method has a static return type declaration, a new test double is created so that it can be returned
  • The destructor of this test double is triggered, which in turn wants to call another method on that test double object, which does not work because the test double state has not been propery initialized yet

I was not able to reproduce in a minimal test case (without Process) whether running the destructor of a test double object created by the return value generator and where the destructor calls a doubled method is actually the problem, but at least some evidence points there. This needs more debugging, for which I currently do not have the time, sorry.

If this really is the case, then yes: you have a found a bug. However, I suspect that you assume in your test that $p->setWorkingDirectory("foo") returns a reference to $p, along the lines of the fluent API of this class. This was never the case. If you are doubling an object that provides a fluent API you need to configure the return value of the methods you use in your test accordingly using willReturnSelf():

class IssueTest extends TestCase
{
    public function testBug()
    {
        $p = $this->createMock(Issue::class);

        $p
            ->method('setWorkingDirectory')
            ->willReturnSelf();

        self::assertInstanceOf(
            Issue::class,
            $p->setWorkingDirectory("foo"),
        );
    }
}

(#5874 (comment) happened in the middle of me debugging the issue and writing this comment)

@frenchcomp
Copy link
Author

My repo, you have an example without Process, only with PHPUnit and a sample code reproducing the error, and I have always the issue.

  1. Test\IssueTest::testBug
    Error: Typed property MockObject_Issue_cfe8d93d::$__phpunit_state must not be accessed before initialization

/home/richard/Bureau/test_phpunit_11_2_process/src/Issue.php:11
/home/richard/Bureau/test_phpunit_11_2_process/src/IssueTest.php:20

@frenchcomp
Copy link
Author

I got it.


    public function callWithStaticAsReturnType(string $cwd): static
    {
        return $this;
    }

    public function callWithSelfAsReturnType(string $cwd): self
    {
        return $this;
    }

With "self" as return type, i have no error, but with static i have an error

@sebastianbergmann sebastianbergmann changed the title Typed property MockObject_Process_be57136f::$__phpunit_state must not be accessed before initialization error when doubling Symfony\Component\Process\Process Doubled methods cannot be called from destructor on test double created by the return value generator Jun 19, 2024
sebastianbergmann added a commit that referenced this issue Jun 19, 2024
@sebastianbergmann
Copy link
Owner

Interestingly, the return value generator has a special case for static but not for self:

https://github.com/sebastianbergmann/phpunit/blob/11.2.3/src/Framework/MockObject/Runtime/ReturnValueGenerator.php#L95-L97

@sebastianbergmann
Copy link
Owner

Thank you, @frenchcomp, for bringing this to my attention. I hope to be able to look into this more closely soon.

@frenchcomp
Copy link
Author

You' re welcome. Thank you for you works. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature/test-doubles Stubs and Mock Objects type/bug Something is broken version/11 Something affects PHPUnit 11
Projects
None yet
Development

No branches or pull requests

2 participants