Skip to content

Commit 85433d6

Browse files
committed
addd attribute if msising method
1 parent b7c3087 commit 85433d6

File tree

2 files changed

+91
-12
lines changed

2 files changed

+91
-12
lines changed

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: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PhpParser\Node\Stmt\ClassMethod;
1414
use PHPStan\Reflection\ReflectionProvider;
1515
use Rector\Doctrine\NodeAnalyzer\AttributeFinder;
16+
use Rector\PhpParser\Node\BetterNodeFinder;
1617
use Rector\PHPUnit\Enum\PHPUnitAttribute;
1718
use Rector\PHPUnit\Enum\PHPUnitClassName;
1819
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
@@ -31,7 +32,8 @@ final class AllowMockObjectsWithoutExpectationsAttributeRector extends AbstractR
3132
public function __construct(
3233
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
3334
private readonly AttributeFinder $attributeFinder,
34-
private readonly ReflectionProvider $reflectionProvider
35+
private readonly ReflectionProvider $reflectionProvider,
36+
private readonly BetterNodeFinder $betterNodeFinder,
3537
) {
3638
}
3739

@@ -56,22 +58,45 @@ public function refactor(Node $node): ?Class_
5658
return null;
5759
}
5860

59-
// @todo add the attribute if has more than 1 public test* method
61+
$missedTestMethodsByMockPropertyName = [];
62+
$usingTestMethodsByMockPropertyName = [];
6063
$testMethodCount = 0;
6164

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
65+
foreach ($mockObjectPropertyNames as $mockObjectPropertyName) {
66+
$missedTestMethodsByMockPropertyName[$mockObjectPropertyName] = [];
67+
$usingTestMethodsByMockPropertyName[$mockObjectPropertyName] = [];
68+
69+
foreach ($node->getMethods() as $classMethod) {
70+
if (! $this->testsNodeAnalyzer->isTestClassMethod($classMethod)) {
71+
continue;
72+
}
6673

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

87+
if (! $this->shouldAddAttribute($missedTestMethodsByMockPropertyName)) {
88+
return null;
89+
}
90+
91+
// skip sole test method, as those are expected to use all mocks
7192
if ($testMethodCount < 2) {
7293
return null;
7394
}
7495

96+
if (! $this->isAtLeastOneMockPropertyMockedOnce($usingTestMethodsByMockPropertyName)) {
97+
return null;
98+
}
99+
75100
// add attribute
76101
$node->attrGroups[] = new AttributeGroup([
77102
new Attribute(new FullyQualified(PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS)),
@@ -83,7 +108,7 @@ public function refactor(Node $node): ?Class_
83108
public function getRuleDefinition(): RuleDefinition
84109
{
85110
return new RuleDefinition(
86-
'Add #[AllowMockObjectsWithoutExpectations] attribute to PHPUnit test classes with mock properties used in multiple methods',
111+
'Add #[AllowMockObjectsWithoutExpectations] attribute to PHPUnit test classes with mock properties used in multiple methods but one, to avoid irrelevant notices in tests run',
87112
[
88113
new CodeSample(
89114
<<<'CODE_SAMPLE'
@@ -125,20 +150,22 @@ protected function setUp(): void
125150
126151
public function testOne(): void
127152
{
128-
// use $this->someServiceMock
153+
$this->someServiceMock->expects($this->once())
154+
->method('someMethod')
155+
->willReturn('someValue');
129156
}
130157
131158
public function testTwo(): void
132159
{
133-
// use $this->someServiceMock
160+
$this->someServiceMock->expects($this->once())
161+
->method('someMethod')
162+
->willReturn('anotherValue');
134163
}
135164
}
136165
CODE_SAMPLE
137166
),
138-
139167
]
140168
);
141-
142169
}
143170

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

0 commit comments

Comments
 (0)