Skip to content

Commit

Permalink
[PHPUnit] Add NoTestMocksRule (#88)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Sep 29, 2023
1 parent 857d617 commit ef4fcba
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 3 deletions.
40 changes: 39 additions & 1 deletion docs/rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 56 Rules Overview
# 57 Rules Overview

## AnnotateRegexClassConstWithRegexLinkRule

Expand Down Expand Up @@ -1519,6 +1519,44 @@ final class SomeClass

<br>

## NoTestMocksRule

Mocking "%s" class is forbidden. Use direct/anonymous class instead for better static analysis

- class: [`Symplify\PHPStanRules\Rules\PHPUnit\NoTestMocksRule`](../src/Rules/PHPUnit/NoTestMocksRule.php)

```php
use PHPUnit\Framework\TestCase;

final class SkipApiMock extends TestCase
{
public function test()
{
$someTypeMock = $this->createMock(SomeType::class);
}
}
```

:x:

<br>

```php
use PHPUnit\Framework\TestCase;

final class SkipApiMock extends TestCase
{
public function test()
{
$someTypeMock = new class() implements SomeType {};
}
}
```

:+1:

<br>

## NoVoidGetterMethodRule

Getter method must return something, not void
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ parameters:

# part of public contract
- '#Public constant "Symplify\\PHPStanRules\\(.*?)\:\:ERROR_MESSAGE" is never used#'
- '#Method Symplify\\PHPStanRules\\Tests\\Rules\\PHPUnit\\(.*?)\\(.*?)Test\:\:testRule\(\) has parameter \$expectedErrorMessagesWithLines with no value type specified in iterable type array#'
136 changes: 136 additions & 0 deletions src/Rules/PHPUnit/NoTestMocksRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Rules\PHPUnit;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Type\ObjectType;
use Symplify\RuleDocGenerator\Contract\DocumentedRuleInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @implements Rule<MethodCall>
*/
final class NoTestMocksRule implements Rule, DocumentedRuleInterface
{
/**
* @api
* @var string
*/
public const ERROR_MESSAGE = 'Mocking "%s" class is forbidden. Use direct/anonymous class instead for better static analysis';

/**
* @var string[]
*/
private const MOCKING_METHOD_NAMES = ['createMock', 'createPartialMock', 'createConfiguredMock', 'createStub'];

/**
* @param string[] $allowedTypes
*/
public function __construct(
private array $allowedTypes = []
) {
}

/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return MethodCall::class;
}

/**
* @param MethodCall $node
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $node->name instanceof Identifier) {
return [];
}

$methodName = $node->name->toString();
if (! in_array($methodName, self::MOCKING_METHOD_NAMES, true)) {
return [];
}

$mockedObjectType = $this->resolveMockedObjectType($node, $scope);
if (! $mockedObjectType instanceof ObjectType) {
return [];
}

if ($this->isAllowedType($mockedObjectType)) {
return [];
}

$errorMessage = sprintf(self::ERROR_MESSAGE, $mockedObjectType->getClassName());

return [$errorMessage];
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(self::ERROR_MESSAGE, [
new CodeSample(
<<<'CODE_SAMPLE'
use PHPUnit\Framework\TestCase;
final class SkipApiMock extends TestCase
{
public function test()
{
$someTypeMock = $this->createMock(SomeType::class);
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use PHPUnit\Framework\TestCase;
final class SkipApiMock extends TestCase
{
public function test()
{
$someTypeMock = new class() implements SomeType {};
}
}
CODE_SAMPLE
),
]);
}

private function resolveMockedObjectType(MethodCall $methodCall, Scope $scope): ?ObjectType
{
$args = $methodCall->getArgs();

$mockedArgValue = $args[0]->value;
$variableType = $scope->getType($mockedArgValue);

foreach ($variableType->getConstantStrings() as $constantString) {
return new ObjectType($constantString->getValue());
}

return null;
}

private function isAllowedType(ObjectType $objectType): bool
{
foreach ($this->allowedTypes as $allowedType) {
if ($objectType->getClassName() === $allowedType) {
return true;
}

if ($objectType->isInstanceOf($allowedType)->yes()) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Ares\PHPStan\Tests\Rule\NoReturnFalseInNonBoolClassMethodRule\Fixture;
namespace Symplify\PHPStanRules\Tests\Rule\NoReturnFalseInNonBoolClassMethodRule\Fixture;

final class ReturnFalseOnly
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Ares\PHPStan\Tests\Rule\NoReturnFalseInNonBoolClassMethodRule\Fixture;
namespace Symplify\PHPStanRules\Tests\Rule\NoReturnFalseInNonBoolClassMethodRule\Fixture;

final class SkipReturnBool
{
Expand Down
16 changes: 16 additions & 0 deletions tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SkipApiMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\PHPUnit\NoTestMocksRule\Fixture;

use PHPUnit\Framework\TestCase;
use Symplify\PHPStanRules\Tests\PHPUnit\Rules\NoTestMocksRule\Source\SomeAllowedType;

final class SkipApiMock extends TestCase
{
public function test()
{
$someAllowedTypeMock = $this->createMock(SomeAllowedType::class);
}
}
15 changes: 15 additions & 0 deletions tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SomeMocking.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\PHPUnit\Rules\NoTestMocksRule\Fixture;

use PHPUnit\Framework\TestCase;

final class SomeMocking extends TestCase
{
public function test()
{
$someClassMock = $this->createMock('SomeClass');
}
}
45 changes: 45 additions & 0 deletions tests/Rules/PHPUnit/NoTestMocksRule/NoTestMocksRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\PHPUnit\NoTestMocksRule;

use Iterator;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use Symplify\PHPStanRules\Rules\PHPUnit\NoTestMocksRule;

final class NoTestMocksRuleTest extends RuleTestCase
{
#[DataProvider('provideData')]
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
{
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
}

public static function provideData(): Iterator
{
yield [
__DIR__ . '/Fixture/SomeMocking.php',
[[sprintf(NoTestMocksRule::ERROR_MESSAGE, 'SomeClass'), 13]],
];

yield [__DIR__ . '/Fixture/SkipApiMock.php', []];
}

/**
* @return string[]
*/
public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/config/configured_rule.neon',
];
}

protected function getRule(): Rule
{
return self::getContainer()->getByType(NoTestMocksRule::class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Symplify\PHPStanRules\Tests\PHPUnit\Rules\NoTestMocksRule\Source;

class SomeAllowedType
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
-
class: Symplify\PHPStanRules\Rules\PHPUnit\NoTestMocksRule
tags: [phpstan.rules.rule]
arguments:
allowedTypes:
- Symplify\PHPStanRules\Tests\PHPUnit\Rules\NoTestMocksRule\Source\SomeAllowedType

0 comments on commit ef4fcba

Please sign in to comment.