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

Add onlyMethods + addMethods and soft deprecate setMethods #3687

Closed
Closed
6 changes: 5 additions & 1 deletion .psalm/baseline.xml
Expand Up @@ -201,6 +201,9 @@
<PossiblyNullPropertyAssignmentValue occurrences="1">
<code>null</code>
</PossiblyNullPropertyAssignmentValue>
<ArgumentTypeCoercion occurrences="2">
<code>$this->type</code>
</ArgumentTypeCoercion>
</file>
<file src="src/Framework/MockObject/MockMethod.php">
<ArgumentTypeCoercion occurrences="1">
Expand All @@ -217,7 +220,8 @@
</TypeDoesNotContainNull>
</file>
<file src="src/Framework/TestCase.php">
<ArgumentTypeCoercion occurrences="1">
<ArgumentTypeCoercion occurrences="2">
<code>$class_name</code>
<code>$this-&gt;expectedException</code>
</ArgumentTypeCoercion>
<InvalidArgument occurrences="2">
Expand Down
87 changes: 87 additions & 0 deletions src/Framework/MockObject/MockBuilder.php
Expand Up @@ -86,6 +86,11 @@ final class MockBuilder
*/
private $generator;

/**
* @var bool
*/
private $alreadyUsedMockMethodConfiguration = false;

/**
* @param string|string[] $type
*
Expand Down Expand Up @@ -181,11 +186,93 @@ public function getMockForTrait(): MockObject

/**
* Specifies the subset of methods to mock. Default is to mock none of them.
*
* @deprecated https://github.com/sebastianbergmann/phpunit/pull/3687
*/
public function setMethods(array $methods = null): self
{
$this->methods = $methods;

$this->alreadyUsedMockMethodConfiguration = true;

return $this;
}

/**
* Specifies the subset of methods to mock, requiring each to exist in the class
*
* @param string[] $methods
*
* @throws RuntimeException
*/
public function onlyMethods(array $methods): self
{
if ($this->alreadyUsedMockMethodConfiguration) {
throw new RuntimeException(
\sprintf(
'Can\'t use onlyMethods on "%s" mock because mocked methods were already configured.',
$this->type
)
);
}

$this->alreadyUsedMockMethodConfiguration = true;

$reflection = new \ReflectionClass($this->type);

foreach ($methods as $method) {
if (!$reflection->hasMethod($method)) {
throw new RuntimeException(
DFoxinator marked this conversation as resolved.
Show resolved Hide resolved
\sprintf(
'Trying to set mock method "%s" with onlyMethods, but it does not exist in class "%s". Use addMethods() for methods that don\'t exist in the class.',
$method,
$this->type
)
);
}
}

$this->methods = $methods;

return $this;
}

/**
* Specifies methods that don't exist in the class which you want to mock
*
* @param string[] $methods
*
* @throws RuntimeException
*/
public function addMethods(array $methods): self
{
if ($this->alreadyUsedMockMethodConfiguration) {
throw new RuntimeException(
\sprintf(
'Can\'t use addMethods on "%s" mock because mocked methods were already configured.',
$this->type
)
);
}

$this->alreadyUsedMockMethodConfiguration = true;

$reflection = new \ReflectionClass($this->type);

foreach ($methods as $method) {
if ($reflection->hasMethod($method)) {
throw new RuntimeException(
\sprintf(
'Trying to set mock method "%s" with addMethod, but it exists in class "%s". Use onlyMethods() for methods that exist in the class.',
$method,
$this->type
)
);
}
}

$this->methods = $methods;

return $this;
}

Expand Down
20 changes: 20 additions & 0 deletions src/Framework/TestCase.php
Expand Up @@ -1521,6 +1521,26 @@ protected function createConfiguredMock($originalClassName, array $configuration
*/
protected function createPartialMock($originalClassName, array $methods): MockObject
{
$class_names = \is_array($originalClassName) ? $originalClassName : [$originalClassName];

foreach ($class_names as $class_name) {
$reflection = new \ReflectionClass($class_name);

$mockedMethodsThatDontExist = \array_filter($methods, function (string $method) use ($reflection) {
return !$reflection->hasMethod($method);
});

if ($mockedMethodsThatDontExist) {
$this->addWarning(
\sprintf(
'createPartialMock called with method(s) %s that do not exist in %s. This will not be allowed in future versions of PHPUnit.',
\implode(', ', $mockedMethodsThatDontExist),
$class_name
)
);
}
}

return $this->getMockBuilder($originalClassName)
->disableOriginalConstructor()
->disableOriginalClone()
Expand Down
11 changes: 11 additions & 0 deletions tests/_files/TestWithDifferentStatuses.php
Expand Up @@ -45,4 +45,15 @@ public function testThatAddsAWarning(): void
{
$this->addWarning('Sorry, Dave!');
}

public function testWithCreatePartialMockWarning(): void
{
$this->createPartialMock(\Mockable::class, ['mockableMethod', 'fakeMethod1', 'fakeMethod2']);
}

public function testWithCreatePartialMockPassesNoWarning(): void
{
$mock = $this->createPartialMock(\Mockable::class, ['mockableMethod']);
$this->assertNull($mock->mockableMethod());
}
}
107 changes: 107 additions & 0 deletions tests/unit/Framework/MockObject/MockBuilderTest.php
Expand Up @@ -51,6 +51,53 @@ public function testMethodExceptionsToMockCanBeSpecified(): void
$this->assertNull($mock->anotherMockableMethod());
}

public function testSetMethodsAllowsNonExistentMethodNames(): void
{
$mock = $this->getMockBuilder(Mockable::class)
->setMethods(['mockableMethodWithCrazyName'])
->getMock();

$this->assertNull($mock->mockableMethodWithCrazyName());
}

public function testOnlyMethodsWithNonExistentMethodNames(): void
{
$this->expectException(RuntimeException::class);

$this->getMockBuilder(Mockable::class)
->onlyMethods(['mockableMethodWithCrazyName'])
->getMock();
}

public function testOnlyMethodsWithExistingMethodNames(): void
{
$mock = $this->getMockBuilder(Mockable::class)
->onlyMethods(['mockableMethod'])
->getMock();

$this->assertNull($mock->mockableMethod());
$this->assertTrue($mock->anotherMockableMethod());
}

public function testAddMethodsWithNonExistentMethodNames(): void
{
$this->expectException(RuntimeException::class);

$this->getMockBuilder(Mockable::class)
->addMethods(['mockableMethod'])
->getMock();
}

public function testAddMethodsWithExistingMethodNames(): void
{
$mock = $this->getMockBuilder(Mockable::class)
->addMethods(['mockableMethodWithFakeMethod'])
->getMock();

$this->assertNull($mock->mockableMethodWithFakeMethod());
$this->assertTrue($mock->anotherMockableMethod());
}

public function testEmptyMethodExceptionsToMockCanBeSpecified(): void
{
$mock = $this->getMockBuilder(Mockable::class)
Expand All @@ -61,6 +108,66 @@ public function testEmptyMethodExceptionsToMockCanBeSpecified(): void
$this->assertNull($mock->anotherMockableMethod());
}

public function testNotAbleToUseAddMethodsAfterOnlyMethods(): void
{
$this->expectException(RuntimeException::class);

$this->getMockBuilder(Mockable::class)
->onlyMethods(['mockableMethod'])
->addMethods(['mockableMethodWithFakeMethod'])
->getMock();
}

public function testNotAbleToUseOnlyMethodsAfterAddMethods(): void
{
$this->expectException(RuntimeException::class);

$this->getMockBuilder(Mockable::class)
->addMethods(['mockableMethodWithFakeMethod'])
->onlyMethods(['mockableMethod'])
->getMock();
}

public function testAbleToUseSetMethodsAfterOnlyMethods(): void
{
$mock = $this->getMockBuilder(Mockable::class)
->onlyMethods(['mockableMethod'])
->setMethods(['mockableMethodWithCrazyName'])
->getMock();

$this->assertNull($mock->mockableMethodWithCrazyName());
}

public function testAbleToUseSetMethodsAfterAddMethods(): void
{
$mock = $this->getMockBuilder(Mockable::class)
->addMethods(['notAMethod'])
->setMethods(['mockableMethodWithCrazyName'])
->getMock();

$this->assertNull($mock->mockableMethodWithCrazyName());
}

public function testNotAbleToUseAddMethodsAfterSetMethods(): void
{
$this->expectException(RuntimeException::class);

$this->getMockBuilder(Mockable::class)
->setMethods(['mockableMethod'])
->addMethods(['mockableMethodWithFakeMethod'])
->getMock();
}

public function testNotAbleToUseOnlyMethodsAfterSetMethods(): void
{
$this->expectException(RuntimeException::class);

$this->getMockBuilder(Mockable::class)
->setMethods(['mockableMethodWithFakeMethod'])
->onlyMethods(['mockableMethod'])
->getMock();
}

public function testByDefaultDoesNotPassArgumentsToTheConstructor(): void
{
$mock = $this->getMockBuilder(Mockable::class)->getMock();
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/Framework/TestCaseTest.php
Expand Up @@ -857,6 +857,26 @@ public function testCreatePartialMockCanMockNoMethods(): void
$this->assertTrue($mock->anotherMockableMethod());
}

public function testCreatePartialMockWithFakeMethods(): void
{
$test = new \TestWithDifferentStatuses('testWithCreatePartialMockWarning');

$test->run();

$this->assertSame(BaseTestRunner::STATUS_WARNING, $test->getStatus());
$this->assertFalse($test->hasFailed());
}

public function testCreatePartialMockWithRealMethods(): void
{
$test = new \TestWithDifferentStatuses('testWithCreatePartialMockPassesNoWarning');

$test->run();

$this->assertSame(BaseTestRunner::STATUS_PASSED, $test->getStatus());
$this->assertFalse($test->hasFailed());
}

public function testCreateMockSkipsConstructor(): void
{
/** @var \Mockable $mock */
Expand Down