Skip to content

Commit 0a60606

Browse files
authored
[phpunit] Add NoDoubleConsecutiveTestMockRule (#219)
1 parent 4215d17 commit 0a60606

File tree

6 files changed

+149
-0
lines changed

6 files changed

+149
-0
lines changed

config/phpunit-rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ rules:
77
- Symplify\PHPStanRules\Rules\Doctrine\NoDocumentMockingRule
88
- Symplify\PHPStanRules\Rules\Doctrine\NoEntityMockingRule
99
- Symplify\PHPStanRules\Rules\PHPUnit\NoMockObjectAndRealObjectPropertyRule
10+
- Symplify\PHPStanRules\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule

src/Enum/RuleIdentifier/PHPUnitRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ final class PHPUnitRuleIdentifier
1515
public const NO_MOCK_OBJECT_AND_REAL_OBJECT_PROPERTY = 'phpunit.noMockObjectAndRealObjectProperty';
1616

1717
public const NO_ASSERT_FUNC_CALL_IN_TESTS = 'phpunit.noAssertFuncCallInTests';
18+
19+
public const NO_DOUBLE_CONSECUTIVE_TEST_MOCK = 'phpunit.noDoubleConsecutiveTestMock';
1820
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\PHPUnit;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleError;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use Rector\PHPUnit\Enum\PHPUnitClassName;
14+
use Symplify\PHPStanRules\Enum\RuleIdentifier\PHPUnitRuleIdentifier;
15+
use Symplify\PHPStanRules\Helper\NamingHelper;
16+
17+
/**
18+
* @implements Rule<MethodCall>
19+
*/
20+
final class NoDoubleConsecutiveTestMockRule implements Rule
21+
{
22+
/**
23+
* @var string
24+
*/
25+
public const ERROR_MESSAGE = 'Do not use "willReturnOnConsecutiveCalls()" and "willReturnCallback()" on the same mock. Use "willReturnCallback() only instead to make test more clear.';
26+
27+
public function getNodeType(): string
28+
{
29+
return MethodCall::class;
30+
}
31+
32+
/**
33+
* @param MethodCall $node
34+
* @return RuleError[]
35+
*/
36+
public function processNode(Node $node, Scope $scope): array
37+
{
38+
// 1. detect if we're in a PHPUnit test case
39+
if (! $scope->isInClass()) {
40+
return [];
41+
}
42+
43+
$classReflection = $scope->getClassReflection();
44+
if (! $classReflection->is(PHPUnitClassName::TEST_CASE)) {
45+
return [];
46+
}
47+
48+
// 2. find a phpunit mock call, that uses "willReturnOnConsecutiveCalls" and "willReturnCallback" on the same line
49+
if (! $node->var instanceof MethodCall) {
50+
return [];
51+
}
52+
53+
$parentCall = $node->var;
54+
if (! $this->containsBothMethodCallNames($node, $parentCall)) {
55+
return [];
56+
}
57+
58+
$identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
59+
->identifier(PHPUnitRuleIdentifier::NO_DOUBLE_CONSECUTIVE_TEST_MOCK)
60+
->build();
61+
62+
return [$identifierRuleError];
63+
}
64+
65+
private function containsBothMethodCallNames(MethodCall $firstMethodCall, MethodCall $secondMethodCall): bool
66+
{
67+
if (NamingHelper::isName($firstMethodCall->name, 'willReturnOnConsecutiveCalls') && NamingHelper::isName($secondMethodCall->name, 'willReturnCallback')) {
68+
return true;
69+
}
70+
71+
return NamingHelper::isName($secondMethodCall->name, 'willReturnOnConsecutiveCalls') && NamingHelper::isName($firstMethodCall->name, 'willReturnCallback');
72+
}
73+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class SkipWillReturnCallback extends TestCase
8+
{
9+
public function test()
10+
{
11+
$this->createMock('SomeClass')
12+
->expects($this->exactly(2))
13+
->method('someMethod')
14+
->willReturnCallback(
15+
function () {
16+
return 'first';
17+
}
18+
);
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class WillReturnAndWithConsecutive extends TestCase
8+
{
9+
public function test()
10+
{
11+
$this->createMock('SomeClass')
12+
->expects($this->exactly(2))
13+
->method('someMethod')
14+
->willReturnCallback(
15+
function () {
16+
return 'first';
17+
}
18+
)
19+
->willReturnOnConsecutiveCalls('first');
20+
}
21+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule;
12+
13+
final class NoDoubleConsecutiveTestMockRuleTest extends RuleTestCase
14+
{
15+
#[DataProvider('provideData')]
16+
public function testRule(string $filePath, array $expectedErrorsWithLines): void
17+
{
18+
$this->analyse([$filePath], $expectedErrorsWithLines);
19+
}
20+
21+
public static function provideData(): Iterator
22+
{
23+
yield [__DIR__ . '/Fixture/WillReturnAndWithConsecutive.php', [[NoDoubleConsecutiveTestMockRule::ERROR_MESSAGE, 11]]];
24+
25+
yield [__DIR__ . '/Fixture/SkipWillReturnCallback.php', []];
26+
}
27+
28+
protected function getRule(): Rule
29+
{
30+
return new NoDoubleConsecutiveTestMockRule();
31+
}
32+
}

0 commit comments

Comments
 (0)