Skip to content

Commit 236d186

Browse files
committed
addd attribute if msising method
1 parent b7c3087 commit 236d186

File tree

3 files changed

+93
-13
lines changed

3 files changed

+93
-13
lines changed

config/sets/phpunit120.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Rector\Config\RectorConfig;
66
use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector;
7-
use Rector\PHPUnit\PHPUnit120\Rector\Class_\AllowMockObjectsWithoutExpectationsAttributeRector;
87
use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector;
98
use Rector\PHPUnit\PHPUnit120\Rector\Class_\RemoveOverrideFinalConstructTestCaseRector;
109

rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,5 @@ final class SkipIfMockNotUsedIn2TestMethods extends TestCase
1919

2020
public function testTwo()
2121
{
22-
2322
}
2423
}

rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
use PhpParser\Node;
88
use PhpParser\Node\Attribute;
99
use PhpParser\Node\AttributeGroup;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\Node\Expr\PropertyFetch;
1012
use PhpParser\Node\Name;
1113
use PhpParser\Node\Name\FullyQualified;
1214
use PhpParser\Node\Stmt\Class_;
1315
use PhpParser\Node\Stmt\ClassMethod;
1416
use PHPStan\Reflection\ReflectionProvider;
1517
use Rector\Doctrine\NodeAnalyzer\AttributeFinder;
18+
use Rector\PhpParser\Node\BetterNodeFinder;
1619
use Rector\PHPUnit\Enum\PHPUnitAttribute;
1720
use Rector\PHPUnit\Enum\PHPUnitClassName;
1821
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
@@ -31,7 +34,8 @@ final class AllowMockObjectsWithoutExpectationsAttributeRector extends AbstractR
3134
public function __construct(
3235
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
3336
private readonly AttributeFinder $attributeFinder,
34-
private readonly ReflectionProvider $reflectionProvider
37+
private readonly ReflectionProvider $reflectionProvider,
38+
private readonly BetterNodeFinder $betterNodeFinder,
3539
) {
3640
}
3741

@@ -56,22 +60,45 @@ public function refactor(Node $node): ?Class_
5660
return null;
5761
}
5862

59-
// @todo add the attribute if has more than 1 public test* method
63+
$missedTestMethodsByMockPropertyName = [];
64+
$usingTestMethodsByMockPropertyName = [];
6065
$testMethodCount = 0;
6166

62-
foreach ($node->getMethods() as $classMethod) {
63-
if ($this->testsNodeAnalyzer->isTestClassMethod($classMethod)) {
64-
// is a mock property used in the method?
65-
// skip if so
67+
foreach ($mockObjectPropertyNames as $mockObjectPropertyName) {
68+
$missedTestMethodsByMockPropertyName[$mockObjectPropertyName] = [];
69+
$usingTestMethodsByMockPropertyName[$mockObjectPropertyName] = [];
70+
71+
foreach ($node->getMethods() as $classMethod) {
72+
if (! $this->testsNodeAnalyzer->isTestClassMethod($classMethod)) {
73+
continue;
74+
}
6675

6776
++$testMethodCount;
77+
78+
// is a mock property used in the class method, as part of some method call? guessing mock expectation is set
79+
// skip if so
80+
if ($this->isClassMethodUsingMethodCallOnPropertyNamed($classMethod, $mockObjectPropertyName)) {
81+
$usingTestMethodsByMockPropertyName[$mockObjectPropertyName][] = $this->getName($classMethod);
82+
continue;
83+
}
84+
85+
$missedTestMethodsByMockPropertyName[$mockObjectPropertyName][] = $this->getName($classMethod);
6886
}
6987
}
7088

89+
if (! $this->shouldAddAttribute($missedTestMethodsByMockPropertyName)) {
90+
return null;
91+
}
92+
93+
// skip sole test method, as those are expected to use all mocks
7194
if ($testMethodCount < 2) {
7295
return null;
7396
}
7497

98+
if (! $this->isAtLeastOneMockPropertyMockedOnce($usingTestMethodsByMockPropertyName)) {
99+
return null;
100+
}
101+
75102
// add attribute
76103
$node->attrGroups[] = new AttributeGroup([
77104
new Attribute(new FullyQualified(PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS)),
@@ -83,7 +110,7 @@ public function refactor(Node $node): ?Class_
83110
public function getRuleDefinition(): RuleDefinition
84111
{
85112
return new RuleDefinition(
86-
'Add #[AllowMockObjectsWithoutExpectations] attribute to PHPUnit test classes with mock properties used in multiple methods',
113+
'Add #[AllowMockObjectsWithoutExpectations] attribute to PHPUnit test classes with mock properties used in multiple methods but one, to avoid irrelevant notices in tests run',
87114
[
88115
new CodeSample(
89116
<<<'CODE_SAMPLE'
@@ -125,20 +152,22 @@ protected function setUp(): void
125152
126153
public function testOne(): void
127154
{
128-
// use $this->someServiceMock
155+
$this->someServiceMock->expects($this->once())
156+
->method('someMethod')
157+
->willReturn('someValue');
129158
}
130159
131160
public function testTwo(): void
132161
{
133-
// use $this->someServiceMock
162+
$this->someServiceMock->expects($this->once())
163+
->method('someMethod')
164+
->willReturn('anotherValue');
134165
}
135166
}
136167
CODE_SAMPLE
137168
),
138-
139169
]
140170
);
141-
142171
}
143172

144173
/**
@@ -187,4 +216,57 @@ private function shouldSkipClass(Class_ $class): bool
187216
$setupClassMethod = $class->getMethod(MethodName::SET_UP);
188217
return ! $setupClassMethod instanceof ClassMethod;
189218
}
219+
220+
private function isClassMethodUsingMethodCallOnPropertyNamed(
221+
ClassMethod $classMethod,
222+
string $mockObjectPropertyName
223+
): bool {
224+
/** @var MethodCall[] $methodCalls */
225+
$methodCalls = $this->betterNodeFinder->findInstancesOfScoped([$classMethod], [MethodCall::class]);
226+
foreach ($methodCalls as $methodCall) {
227+
if (! $methodCall->var instanceof PropertyFetch) {
228+
continue;
229+
}
230+
231+
$propertyFetch = $methodCall->var;
232+
233+
// we found a method call on a property fetch named
234+
if ($this->isName($propertyFetch, $mockObjectPropertyName)) {
235+
return true;
236+
}
237+
}
238+
239+
return false;
240+
}
241+
242+
/**
243+
* @param array<string, string[]> $missedTestMethodsByMockPropertyName
244+
*/
245+
private function shouldAddAttribute(array $missedTestMethodsByMockPropertyName): bool
246+
{
247+
foreach ($missedTestMethodsByMockPropertyName as $missedTestMethods) {
248+
// all test methods are using method calls on the mock property, so skip
249+
if (count($missedTestMethods) === 0) {
250+
continue;
251+
}
252+
253+
return true;
254+
}
255+
256+
return false;
257+
}
258+
259+
/**
260+
* @param array<string, string[]> $usingTestMethodsByMockPropertyName
261+
*/
262+
private function isAtLeastOneMockPropertyMockedOnce(array $usingTestMethodsByMockPropertyName): bool
263+
{
264+
foreach ($usingTestMethodsByMockPropertyName as $usingTestMethods) {
265+
if (count($usingTestMethods) !== 0) {
266+
return true;
267+
}
268+
}
269+
270+
return false;
271+
}
190272
}

0 commit comments

Comments
 (0)