Skip to content

Commit

Permalink
Add rector rule for mocking fetches in tests
Browse files Browse the repository at this point in the history
Currently only supports mocking single fetches.
  • Loading branch information
jtojnar committed Jan 7, 2022
1 parent 10ce52d commit 5dbff5e
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 0 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
},
"autoload-dev": {
"psr-4": {
"Maintenance\\Graby\\": "maintenance/",
"Tests\\Graby\\": "tests/"
}
},
Expand Down
109 changes: 109 additions & 0 deletions maintenance/Rector/AddTestHelpersTraitRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace Maintenance\Graby\Rector;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use Rector\Core\NodeManipulator\ClassInsertManipulator;
use Rector\Core\NodeManipulator\ClassManipulator;
use Rector\Core\Rector\AbstractRector;
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class AddTestHelpersTraitRector extends AbstractRector
{
/**
* @var string
*/
private const TEST_HELPERS_TRAIT = 'Tests\Graby\TestHelpers';

private ClassInsertManipulator $classInsertManipulator;
private ClassManipulator $classManipulator;
private TestsNodeAnalyzer $testsNodeAnalyzer;

public function __construct(
ClassInsertManipulator $classInsertManipulator,
ClassManipulator $classManipulator,
TestsNodeAnalyzer $testsNodeAnalyzer,
) {
$this->classInsertManipulator = $classInsertManipulator;
$this->classManipulator = $classManipulator;
$this->testsNodeAnalyzer = $testsNodeAnalyzer;
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Add TestHelpers trait for classes using $this->getGrabyWithMock()',
[
new CodeSample(
<<<'CODE_SAMPLE'
use PHPUnit\Framework\TestCase;
final class ExampleTest extends TestCase
{
public function testOne(): void
{
$graby = $this->getGrabyWithMock('https://example.com');
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use PHPUnit\Framework\TestCase;
use Tests\Graby\TestHelpers;
final class ExampleTest extends TestCase
{
use TestHelpers;
public function testOne(): void
{
$graby = $this->getGrabyWithMock('https://example.com');
}
}
CODE_SAMPLE
),
]
);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if ($this->shouldSkipClass($node)) {
return null;
}

$this->classInsertManipulator->addAsFirstTrait($node, self::TEST_HELPERS_TRAIT);

return $node;
}

private function shouldSkipClass(Class_ $class): bool
{
$usesMethod = (bool) $this->betterNodeFinder->findFirst(
$class,
fn (Node $node): bool => $this->testsNodeAnalyzer->isAssertMethodCallName($node, 'getGrabyWithMock')
);

if (!$usesMethod) {
return true;
}

return $this->classManipulator->hasTrait($class, self::TEST_HELPERS_TRAIT);
}
}
143 changes: 143 additions & 0 deletions maintenance/Rector/MockGrabyResponseRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

declare(strict_types=1);

namespace Maintenance\Graby\Rector;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\String_;
use Rector\Core\Rector\AbstractRector;
use Rector\NodeNestingScope\ParentScopeFinder;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class MockGrabyResponseRector extends AbstractRector
{
private ParentScopeFinder $parentScopeFinder;

public function __construct(ParentScopeFinder $parentScopeFinder)
{
$this->parentScopeFinder = $parentScopeFinder;
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Replace Graby instance by one with a mocked requests and stores the response in a fixture.',
[
new CodeSample(
<<<'CODE_SAMPLE'
$graby = new Graby($config);
$res = $graby->fetchContent('http://example.com/foo');
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$graby = $this->getGrabyWithMock(
'/fixtures/content/http___example.com_foo.html',
200,
$config
);
$res = $graby->fetchContent('http://example.com/');
CODE_SAMPLE
),
]
);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [New_::class];
}

/**
* @param New_ $node
*/
public function refactor(Node $node): ?Node
{
$construction = $node;
if (!$this->nodeNameResolver->isName($construction->class, 'Graby\Graby')) {
return null;
}

/** @var Node $parentNode */
$parentNode = $construction->getAttribute(AttributeKey::PARENT_NODE);
if (!($assignment = $parentNode) instanceof Assign) {
return null;
}

if (\count($construction->args) > 1) {
// The Graby instance is already passed a MockHttpClient.
return null;
}

if (!($grabyVariable = $assignment->var) instanceof Variable) {
return null;
}

$scope = $this->parentScopeFinder->find($construction);
if (null === $scope) {
return null;
}

$fetchUrls = array_map(
fn (Node $node): ?string => $this->getFetchUrl($grabyVariable, $node),
$this->betterNodeFinder->find(
(array) $scope->stmts,
fn (Node $foundNode): bool => null !== $this->getFetchUrl($grabyVariable, $foundNode)
)
);

if (1 !== \count($fetchUrls)) {
return null;
}

$url = $fetchUrls[0];
$suffix = preg_match('(\.[a-z0-9]+$)', $url) ? '' : '.html';
$fileName = '/fixtures/content/' . preg_replace('#[^a-zA-Z0-9-_\.]#', '_', $url) . $suffix;

$contents = file_get_contents($url);
file_put_contents(__DIR__ . '/../../tests' . $fileName, $contents);

$args = [new String_($fileName)];
if (\count($construction->args) > 0) {
$args[] = 200;
$args = array_merge($args, $construction->args);
}

return $this->nodeFactory->createLocalMethodCall('getGrabyWithMock', $args);
}

/**
* Extracts URL from the AST node in the form <grabyVariable>->fetchContent('url').
*/
private function getFetchUrl(Variable $grabyVariable, Node $node): ?string
{
if (!($methodCall = $node) instanceof MethodCall) {
return null;
}

if (!$this->nodeNameResolver->areNamesEqual($methodCall->var, $grabyVariable)) {
return null;
}

if (!($methodName = $methodCall->name) instanceof Identifier || !$this->nodeNameResolver->isName($methodName, 'fetchContent')) {
return null;
}

if (1 !== \count($args = $methodCall->args) || !$args[0] instanceof Arg || !($url = $args[0]->value) instanceof String_) {
return null;
}

return $url->value;
}
}
13 changes: 13 additions & 0 deletions maintenance/rector.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

declare(strict_types=1);

use Maintenance\Graby\Rector\AddTestHelpersTraitRector;
use Maintenance\Graby\Rector\MockGrabyResponseRector;
use Rector\Core\Configuration\Option;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\Set\ValueObject\LevelSetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->import(LevelSetList::UP_TO_PHP_74);

$services = $containerConfigurator->services();
$services->set(AddTestHelpersTraitRector::class);
$services->set(MockGrabyResponseRector::class);

$parameters = $containerConfigurator->parameters();
$parameters->set(Option::PATHS, [
__DIR__ . '/../maintenance',
__DIR__ . '/../src',
__DIR__ . '/../tests',
]);
Expand All @@ -22,5 +30,10 @@
$latestPhpunitBridge . '/vendor/autoload.php',
]);

$parameters->set(Option::SKIP, [
// nodeNameResolver requires string.
StringClassNameToClassConstantRector::class => __DIR__ . '/Rector/**',
]);

$parameters->set(Option::PHPSTAN_FOR_RECTOR_PATH, __DIR__ . '/../phpstan.neon');
};
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
parameters:
level: 7
paths:
- maintenance/Rector
- src
- tests

Expand Down

0 comments on commit 5dbff5e

Please sign in to comment.