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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .psalm/baseline.xml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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