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

Mocked methods cannot be called from the original constructor of a partially mocked class #5857

Closed
sbuerk opened this issue Jun 8, 2024 · 4 comments
Assignees
Labels
feature/test-doubles Test Stubs and Mock Objects type/bug Something is broken version/11 Something affects PHPUnit 11

Comments

@sbuerk
Copy link
Contributor

sbuerk commented Jun 8, 2024

Q A
PHPUnit version 11.2.0
PHP version 8.2, 8.3
Installation Method Composer + phpunit repo directly

Summary

A class method is called in the constructor and in other
class methods. Using onlyMethods() to mock that method
does not regonized the calling (count) until now, but at
least subsequential calls could be tested and ensured.

This worked until 11.2.0 of phpunit and was detected in
the TYPO3 nightly pipeline automatically pulling in new
dependency versions, like phpunit.

Note: In manual tests this has been reduced to phpunit
and other dependency ruled out. Can be reproduced within
the phpunit source code directly.

Current behavior

Since 11.2.0 this no longer works and a exception like following
is thrown:

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

How to reproduce

This can be reproduced with a simple class:

<?php declare(strict_types=1);

namespace Vendor\Package;

class ClassCallingMethodInConstructor
{
    public function __construct()
    {
        $this->reset();
    }

    public function reset(): void
    {
    }

    public function second(): void
    {
        $this->reset();
    }
}

and having a test like following:

use Vendor\Package\ClassCallingMethodInConstructor;

public function testOnlyMethodCalledInConstructorWorks(): void
{
  $testClassMock = $this->getMockBuilder(ClassCallingMethodInConstructor::class)
     ->onlyMethods(['reset'])
     ->getMock();

  $testClassMock->expects($this::once())->method('reset');
  $testClassMock->second();
}

Expected behavior

Test passes without error (green), which worked until 11.2.0.

@sbuerk sbuerk added the type/bug Something is broken label Jun 8, 2024
sbuerk added a commit to sbuerk/phpunit that referenced this issue Jun 8, 2024
This change adds a fixture class and a MockBuilder test
to showcase a regression introduced with version 11.2.0.

Given is a class, where one method is mocked using the
`onlyMethods()` option to check the method gets called.
The method is called in the class constructor and also
in other public method.

Providing a test mocking that method this breaks since
11.2.0 - albeit having the count in previous version
only working for calls to the mock method AFTER the
constructor call.

Following exception thrown is not expectd:

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

Related: sebastianbergmann#5857
sbuerk added a commit to sbuerk/phpunit that referenced this issue Jun 8, 2024
This change adds a fixture class and a MockBuilder test
to showcase a regression introduced with version 11.2.0.

Given is a class, where one method is mocked using the
`onlyMethods()` option to check the method gets called.
The method is called in the class constructor and also
in other public method.

Providing a test mocking that method this breaks since
11.2.0 - albeit having the count in previous version
only working for calls to the mock method AFTER the
constructor call.

In 11.1.3 this works and following exception is not recieved:

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

Related: sebastianbergmann#5857
@sebastianbergmann sebastianbergmann added feature/test-doubles Test Stubs and Mock Objects version/11 Something affects PHPUnit 11 labels Jun 9, 2024
@sebastianbergmann sebastianbergmann self-assigned this Jun 9, 2024
@sebastianbergmann
Copy link
Owner

sebastianbergmann commented Jun 9, 2024

PHPUnit 11.2 has added support for extendable classes that are declared readonly to its test double functionality. This required extensive changes to the runtime internals of the test double functionality. These changes are the reason why tests like the one you have provided as an example no longer work. I am grateful to you for reporting this, as I was not aware of this problem before.

Side note: maybe we can discuss at the TYPO3 code sprint this week whether the TYPO3 project can also test in its nightly CI pipeline with the current development version of PHPUnit. This would allow us to uncover such problems earlier.

The problem you found is ultimately due to the fact that the test double functionality of PHPUnit supports not only interfaces, but also extendable classes. The fact that only a selection of the methods of an extendable class can be replaced makes the whole thing even worse.

Please don't get me wrong, I don't want to deny any blame, nor do I want to blame users of PHPUnit. The only person I blame is myself and I am sorry that PHPUnit 11.2 broke your tests.

I will try my best to fix this issue, but at least right now I am not very hopeful that it can be fixed (without reverting #5804).

reviewtypo3org pushed a commit to TYPO3/typo3 that referenced this issue Jun 9, 2024
PHPUnit refactored the double creation to support doubling
readonly classes [1], thus breaking classes where the class
constructor is called with constructor arguments, but calling
itself a class method which is mocked using the `onlyMethods()`
setup option. This leads now to some internal state setup issue
and is reported as `regression` [2] providing steps to reproduce
it.

This change adds `phpunit 11.2.0` as conflict to the monorepo
version to avoid using it in nightly tests for now, until a
proper regression fix has been implemented in phpunit.

Used command(s):

  \
    cat \
    <<< $(jq --indent 1 --tab \
      '."conflict" += {"phpunit/phpunit": "11.2.0"}' \
      composer.json) > composer.json \
    && Build/Scripts/runTests.sh -s composer -- update --lock

[1] sebastianbergmann/phpunit#5804
[2] sebastianbergmann/phpunit#5857

Resolves: #104007
Releases: main
Change-Id: Ia384ff98a9ea4e318738a48bd9ff8afcc4faff15
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84533
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Simon Schaufelberger <simonschaufi+typo3@gmail.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Simon Schaufelberger <simonschaufi+typo3@gmail.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
@sebastianbergmann
Copy link
Owner

I have an idea how to solve this, now I need to find time to give it a try.

@garvinhicking
Copy link
Contributor

Please don't get me wrong, I don't want to deny any blame, nor do I want to blame users of PHPUnit. The only person I blame is myself and I am sorry that PHPUnit 11.2 broke your tests.

Just dropping by to say that there's no blame at all, and those nightly tests are there for us to catch things early, and search for solutions. Thank you and @sbuerk for inspecting this so quickly, and I'm sure there'll be a solution. :-)

sebastianbergmann pushed a commit that referenced this issue Jun 10, 2024
This change adds a fixture class and a MockBuilder test
to showcase a regression introduced with version 11.2.0.

Given is a class, where one method is mocked using the
`onlyMethods()` option to check the method gets called.
The method is called in the class constructor and also
in other public method.

Providing a test mocking that method this breaks since
11.2.0 - albeit having the count in previous version
only working for calls to the mock method AFTER the
constructor call.

Following exception thrown is not expectd:

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

Related: #5857
@sebastianbergmann
Copy link
Owner

Brief update: The confidence Garvin has shown me that I will be able to solve the problem has motivated me to continue thinking about the problem and working on a solution.

I currently have the situation here (local, not committed and pushed) that @sbuerk's test is green. Whoopee! However, several other tests are red. Oh no!

Let's hope that I manage to turn all the tests green again ...

@sebastianbergmann sebastianbergmann changed the title Mocked method called in constructor and a additional method throws an unexpected exception Mocked methods cannot be called from the original constructor of a partially mocked class Jun 10, 2024
reviewtypo3org pushed a commit to TYPO3/typo3 that referenced this issue Jun 11, 2024
The nightly CI run recently revealed a regression
within phpunit [1] which has been fixed meanwhile
upstream [2] by Sebastian Bergmann.

This change raises phpunit to the bugfix version
and the conflict is removed.

Used command(s):

  composer require --dev "phpunit/phpunit":"^11.2.1"

[1] sebastianbergmann/phpunit#5857
[2] https://github.com/sebastianbergmann/phpunit/releases/tag/11.2.1

Resolves: #104036
Releases: main
Change-Id: I8f83ca4b574d174dc0f13bb4a59ad5f35a905204
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84595
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Garvin Hicking <gh@faktor-e.de>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Andreas Kienast <a.fernandez@scripting-base.de>
Reviewed-by: Andreas Kienast <a.fernandez@scripting-base.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature/test-doubles Test Stubs and Mock Objects type/bug Something is broken version/11 Something affects PHPUnit 11
Projects
None yet
Development

No branches or pull requests

3 participants