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

Deprecate MockBuilder::addMethods() #5320

Closed
sebastianbergmann opened this issue Apr 12, 2023 · 7 comments
Closed

Deprecate MockBuilder::addMethods() #5320

sebastianbergmann opened this issue Apr 12, 2023 · 7 comments
Assignees
Labels
feature/test-doubles Test Stubs and Mock Objects type/deprecation Something will be/is deprecated
Milestone

Comments

@sebastianbergmann
Copy link
Owner

To reduce complexity inside PHPUnit's test double functionality, MockBuilder::addMethods() will be deprecated and then removed:

  • soft deprecation in PHPUnit 10.1 (add @deprecated annotation to the method declaration)
  • deprecation in PHPUnit 11 (using the method will trigger a deprecation)
  • removal in PHPUnit 12
@sebastianbergmann sebastianbergmann added feature/test-doubles Test Stubs and Mock Objects type/deprecation Something will be/is deprecated labels Apr 12, 2023
@sebastianbergmann sebastianbergmann added this to the PHPUnit 11.0 milestone Apr 12, 2023
@sebastianbergmann sebastianbergmann self-assigned this Apr 12, 2023
@g105b
Copy link

g105b commented May 4, 2023

In one of my projects, I wanted to use PHPUnit mocks to check that an anonymous function was being called X times. I mocked stdClass and used addMethods() to add the __invoke method, then I could use MockObject::expects() to do the checking.

To fix this deprecation message, I've introduced a new class into my tests directory called MockedCaller, which does the same thing but more explicitly. Here's an example:

class ExampleTest extends MyTestCase {
	public function testAnonFuncIsCalled10Times():void {
		$sut = new MyObject();
// this will pass in a callable to `doSomething10Times` that must be called 10 times with the provided parameters
		$sut->doSomething10Times(self::mockCallable(10, "example param"));
	}
}

class MyTestCase extends TestCase {
	/** @return MockObject|callable */
	protected function mockCallable(int $numCalls = null, ...$expectedParams):MockObject {
		$mock = self::createMock(MockedCaller::class);

		if(!is_null($numCalls)) {
			$expectation = $mock->expects(self::exactly($numCalls))
				->method("__invoke");

			if(!empty($expectedParameters)) {
				foreach($expectedParameters as $p) {
					$expectation->with(self::identicalTo($p));
				}
			}
		}

		return $mock;
	}
}

class MockedCaller {
	public function __invoke():void {}
}

Hope this helps someone!

@pkly
Copy link

pkly commented Aug 21, 2023

I was wondering, what is the correct way of handling this deprecation/removal?
Calling onlyMethods with a method that does not exist produces an exception.

I'm referring to cases where you're mocking an interface which contains a method listed as a phpdoc method for backwards compatibility reasons, and does not exist within the interface itself. I understand it's somewhat of a weird scenario, but still technically valid.

@LordSimal
Copy link

@pkly We use either an anonymous class like this

$entity = new class extends Entity {
    protected function _setName(?string $name): string
    {
        return 'Dr. ' . $name;
    }
};

or straight up another class (inside the TestCase class or in a separate file)

@derrabus
Copy link
Contributor

I'm referring to cases where you're mocking an interface which contains a method listed as a phpdoc method for backwards compatibility reasons, and does not exist within the interface itself.

Declare a new interface that extends that interface and materialize the virtual method.

/**
 * @method void foo(string $a)
 */
interface MyInterface {}

interface MyMockableInterface extends MyInterface
{
    public function foo(string $a): void;
}

$this->createMock(MyMockableInterface::class);

@DanielBadura
Copy link

Is there some other way to mock e.g. calls to PDO::pgsqlGetNotify? Without addMethods I get PHPUnit\Framework\MockObject\MethodCannotBeConfiguredException: Trying to configure method "pgsqlGetNotify" which cannot be configured because it does not exist, has not been specified, is final, or is static.

Or some other suggestions for this case?

@MattesDe
Copy link

How will it be possible to mock methods from a trait in the future if I can no longer specify them with addMethods()?

@NanoSector
Copy link

NanoSector commented Jun 10, 2024

A slightly more flexible solution to @g105b 's MockedCaller class is defining it as a simple generic interface:

/**
 * @see https://github.com/sebastianbergmann/phpunit/issues/5320
 *
 * @template T
 */
interface CallableMock
{
    /**
     * @return T
     */
    public function __invoke();
}

This does not force a void return type, and makes tools like PHPStan happier because you can type hint the callable return type:

        /** @var MockObject&CallableMock<\stdClass> $callableMock */
        $callableMock = $this->createMock(CallableMock::class);

        $callableMock->expects($this->once())
            ->method('__invoke')
            ->willReturnCallback(function (): \stdClass {
                return new \stdClass();
            });

        // Assuming method() has @param callable(): \stdClass $argument
        $result = $someSubject->method($callableMock->__invoke(...));

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/deprecation Something will be/is deprecated
Projects
None yet
Development

No branches or pull requests

8 participants