Skip to content

Commit df52518

Browse files
authored
[phpunit] Add ExplicitExpectsMockMethodRule (#260)
1 parent cb2358f commit df52518

File tree

8 files changed

+169
-18
lines changed

8 files changed

+169
-18
lines changed

config/phpunit-rules.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ rules:
88
- Symplify\PHPStanRules\Rules\Doctrine\NoEntityMockingRule
99
- Symplify\PHPStanRules\Rules\PHPUnit\NoMockObjectAndRealObjectPropertyRule
1010
- Symplify\PHPStanRules\Rules\PHPUnit\NoDoubleConsecutiveTestMockRule
11+
12+
# @todo test first
13+
# ever method() must have expects() call to define how many times it is expected
14+
# - Symplify\PHPStanRules\Rules\PHPUnit\ExplicitExpectsMockMethodRule

phpunit.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313
<exclude>tests/Rules/PHPUnit/PublicStaticDataProviderRule</exclude>
1414
<exclude>tests/Rules/PHPUnit/NoAssertFuncCallInTestsRule/Fixture</exclude>
1515
<exclude>tests/Rules/PHPUnit/NoMockOnlyTestRule/Fixture/SkipConstraintValidatorTest.php</exclude>
16+
<exclude>tests/Rules/PHPUnit/ExplicitExpectsMockMethodRule/Fixture/SkipMockWithExpectsTest.php</exclude>
17+
<exclude>tests/Rules/PHPUnit/ExplicitExpectsMockMethodRule/Fixture/MockWithoutExpectsTest.php</exclude>
1618
</testsuite>
1719
</phpunit>

src/PHPUnit/TestClassDetector.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\PHPUnit;
6+
7+
use PHPStan\Analyser\Scope;
8+
9+
final class TestClassDetector
10+
{
11+
/**
12+
* @var string[]
13+
*/
14+
private const array TEST_FILE_SUFFIXES = [
15+
'Test.php',
16+
'TestCase.php',
17+
'Context.php',
18+
];
19+
20+
public static function isTestClass(Scope $scope): bool
21+
{
22+
foreach (self::TEST_FILE_SUFFIXES as $testFileSuffix) {
23+
if (str_ends_with($scope->getFile(), $testFileSuffix)) {
24+
return true;
25+
}
26+
}
27+
28+
return false;
29+
}
30+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 PhpParser\Node\Expr\PropertyFetch;
10+
use PhpParser\Node\Expr\Variable;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Rules\IdentifierRuleError;
13+
use PHPStan\Rules\Rule;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use Symplify\PHPStanRules\Enum\RuleIdentifier\PHPUnitRuleIdentifier;
16+
use Symplify\PHPStanRules\Helper\NamingHelper;
17+
use Symplify\PHPStanRules\PHPUnit\TestClassDetector;
18+
19+
/**
20+
* @implements Rule<MethodCall>
21+
*
22+
* @see \Symplify\PHPStanRules\Tests\Rules\PHPUnit\ExplicitExpectsMockMethodRule\ExplicitExpectsMockMethodRuleTest
23+
*/
24+
final class ExplicitExpectsMockMethodRule implements Rule
25+
{
26+
public const string ERROR_MESSAGE = 'PHPUnit mock method is missing explicit expects(), e.g. $this->mock->expects($this->once())->...';
27+
28+
public function getNodeType(): string
29+
{
30+
return MethodCall::class;
31+
}
32+
33+
/**
34+
* @param MethodCall $node
35+
* @return IdentifierRuleError[]
36+
*/
37+
public function processNode(Node $node, Scope $scope): array
38+
{
39+
if (! NamingHelper::isName($node->name, 'method')) {
40+
return [];
41+
}
42+
43+
if (! TestClassDetector::isTestClass($scope)) {
44+
return [];
45+
}
46+
47+
if (! $node->var instanceof Variable && ! $node->var instanceof PropertyFetch) {
48+
return [];
49+
}
50+
51+
$callerType = $scope->getType($node->var);
52+
if (! $callerType->hasMethod('expects')->yes()) {
53+
return [];
54+
}
55+
56+
$identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
57+
->identifier(PHPUnitRuleIdentifier::NO_ASSERT_FUNC_CALL_IN_TESTS)
58+
->build();
59+
60+
return [$identifierRuleError];
61+
}
62+
}

src/Rules/PHPUnit/NoAssertFuncCallInTestsRule.php

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Rules\RuleErrorBuilder;
1111
use Symplify\PHPStanRules\Enum\RuleIdentifier\PHPUnitRuleIdentifier;
1212
use Symplify\PHPStanRules\Helper\NamingHelper;
13+
use Symplify\PHPStanRules\PHPUnit\TestClassDetector;
1314

1415
/**
1516
* @implements Rule<FuncCall>
@@ -18,12 +19,6 @@ final class NoAssertFuncCallInTestsRule implements Rule
1819
{
1920
public const string ERROR_MESSAGE = 'Instead of assert() that can miss important checks, use native PHPUnit assert call';
2021

21-
private const array TEST_FILE_SUFFIXES = [
22-
'Test.php',
23-
'TestCase.php',
24-
'Context.php',
25-
];
26-
2722
public function getNodeType(): string
2823
{
2924
return FuncCall::class;
@@ -39,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array
3934
return [];
4035
}
4136

42-
if (! $this->isTestFile($scope)) {
37+
if (! TestClassDetector::isTestClass($scope)) {
4338
return [];
4439
}
4540

@@ -49,15 +44,4 @@ public function processNode(Node $node, Scope $scope): array
4944

5045
return [$identifierRuleError];
5146
}
52-
53-
private function isTestFile(Scope $scope): bool
54-
{
55-
foreach (self::TEST_FILE_SUFFIXES as $testFileSuffix) {
56-
if (str_ends_with($scope->getFile(), $testFileSuffix)) {
57-
return true;
58-
}
59-
}
60-
61-
return false;
62-
}
6347
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\PHPUnit\ExplicitExpectsMockMethodRule;
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\ExplicitExpectsMockMethodRule;
12+
13+
final class ExplicitExpectsMockMethodRuleTest extends RuleTestCase
14+
{
15+
/**
16+
* @param array<int, array<string|int>> $expectedErrorsWithLines
17+
*/
18+
#[DataProvider('provideData')]
19+
public function testRule(string $filePath, array $expectedErrorsWithLines): void
20+
{
21+
$this->analyse([$filePath], $expectedErrorsWithLines);
22+
}
23+
24+
/**
25+
* @return Iterator<array<array<int, mixed>, mixed>>
26+
*/
27+
public static function provideData(): Iterator
28+
{
29+
yield [__DIR__ . '/Fixture/MockWithoutExpectsTest.php', [[ExplicitExpectsMockMethodRule::ERROR_MESSAGE, 12]]];
30+
31+
yield [__DIR__ . '/Fixture/SkipMockWithExpectsTest.php', []];
32+
}
33+
34+
protected function getRule(): Rule
35+
{
36+
return new ExplicitExpectsMockMethodRule();
37+
}
38+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\PHPUnit\ExplicitExpectsMockMethodRule\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class MockWithoutExpectsTest extends TestCase
8+
{
9+
public function test(): void
10+
{
11+
$mock = $this->createMock(\stdClass::class);
12+
$mock->method('someMethod')->willReturn('value');
13+
}
14+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\PHPUnit\ExplicitExpectsMockMethodRule\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class SkipMockWithExpectsTest extends TestCase
8+
{
9+
public function test(): void
10+
{
11+
$mock = $this->createMock(\stdClass::class);
12+
13+
$mock->expects($this->atLeastOnce())
14+
->method('someMethod')
15+
->willReturn('value');
16+
}
17+
}

0 commit comments

Comments
 (0)