Skip to content

Commit

Permalink
handle mocking multiple classes
Browse files Browse the repository at this point in the history
  • Loading branch information
Khartir committed Feb 7, 2020
1 parent e27a765 commit 29b543f
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 20 deletions.
22 changes: 17 additions & 5 deletions src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
Expand All @@ -31,6 +32,7 @@ public function getClass(): string

public function isMethodSupported(MethodReflection $methodReflection): bool
{
$name = $methodReflection->getName();
return array_key_exists($methodReflection->getName(), $this->methods);
}

Expand All @@ -42,15 +44,25 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
return $parametersAcceptor->getReturnType();
}
$argType = $scope->getType($methodCall->args[$argumentIndex]->value);
if (!$argType instanceof ConstantStringType) {
return $parametersAcceptor->getReturnType();

$types = [];
if ($argType instanceof ConstantStringType) {
$types[] = new ObjectType($argType->getValue());
}

if ($argType instanceof ConstantArrayType) {
$types = array_map(function (Type $argType): ObjectType {
return new ObjectType($argType->getValue());
}, $argType->getValueTypes());
}

$class = $argType->getValue();
if (count($types) === 0) {
return $parametersAcceptor->getReturnType();
}

return TypeCombinator::intersect(
new ObjectType($class),
$parametersAcceptor->getReturnType()
$parametersAcceptor->getReturnType(),
...$types
);
}

Expand Down
21 changes: 15 additions & 6 deletions src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
Expand All @@ -30,18 +31,26 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
if (count($methodCall->args) === 0) {
return $mockBuilderType;
}
if (!$mockBuilderType instanceof TypeWithClassName) {
throw new \PHPStan\ShouldNotHappenException();
}

$argType = $scope->getType($methodCall->args[0]->value);
if (!$argType instanceof ConstantStringType) {
return $mockBuilderType;
if ($argType instanceof ConstantStringType) {
$class = $argType->getValue();

return new MockBuilderType($mockBuilderType, $class);
}

$class = $argType->getValue();
if ($argType instanceof ConstantArrayType) {
$classes = array_map(function (Type $argType): string {
return $argType->getValue();
}, $argType->getValueTypes());

if (!$mockBuilderType instanceof TypeWithClassName) {
throw new \PHPStan\ShouldNotHappenException();
return new MockBuilderType($mockBuilderType, ...$classes);
}

return new MockBuilderType($mockBuilderType, $class);
return $mockBuilderType;
}

}
7 changes: 5 additions & 2 deletions src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,13 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
if (!$calledOnType instanceof MockBuilderType) {
return $parametersAcceptor->getReturnType();
}
$types = array_map(function (string $type): ObjectType {
return new ObjectType($type);
}, $calledOnType->getMockedClasses());

return TypeCombinator::intersect(
new ObjectType($calledOnType->getMockedClass()),
$parametersAcceptor->getReturnType()
$parametersAcceptor->getReturnType(),
...$types
);
}

Expand Down
17 changes: 10 additions & 7 deletions src/Type/PHPUnit/MockBuilderType.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,29 @@
class MockBuilderType extends \PHPStan\Type\ObjectType
{

/** @var string */
private $mockedClass;
/** @var array<string> */
private $mockedClasses;

public function __construct(
TypeWithClassName $mockBuilderType,
string $mockedClass
string ...$mockedClasses
)
{
parent::__construct($mockBuilderType->getClassName());
$this->mockedClass = $mockedClass;
$this->mockedClasses = $mockedClasses;
}

public function getMockedClass(): string
/**
* @return array<string>
*/
public function getMockedClasses(): array
{
return $this->mockedClass;
return $this->mockedClasses;
}

public function describe(VerbosityLevel $level): string
{
return sprintf('%s<%s>', parent::describe($level), $this->mockedClass);
return sprintf('%s<%s>', parent::describe($level), implode('&', $this->mockedClasses));
}

}
37 changes: 37 additions & 0 deletions tests/Type/PHPUnit/CreateMockExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\PHPUnit;

use ExampleTestCase\BarInterface;
use ExampleTestCase\FooInterface;
use Iterator;
use PHPUnit\Framework\MockObject\MockObject;

class CreateMockExtensionTest extends ExtensionTestCase
{

/**
* @dataProvider getProvider
* @param string $expression
* @param string $type
*/
public function testCreateMock(string $expression, string $type): void
{
$this->processFile(
__DIR__ . '/data/create-mock.php',
$expression,
$type,
[new CreateMockDynamicReturnTypeExtension(), new GetMockBuilderDynamicReturnTypeExtension()]
);
}

/**
* @return Iterator<mixed>
*/
public function getProvider(): Iterator
{
yield ['$simpleInterface', implode('&', [FooInterface::class, MockObject::class])];
yield ['$doubleInterface', implode('&', [BarInterface::class, FooInterface::class, MockObject::class])];
}

}
77 changes: 77 additions & 0 deletions tests/Type/PHPUnit/ExtensionTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\PHPUnit;

use PhpParser\Node;
use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\ScopeContext;
use PHPStan\Broker\AnonymousClassNameHelper;
use PHPStan\Cache\Cache;
use PHPStan\File\FileHelper;
use PHPStan\Node\VirtualNode;
use PHPStan\PhpDoc\PhpDocNodeResolver;
use PHPStan\PhpDoc\PhpDocStringResolver;
use PHPStan\Testing\TestCase;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\VerbosityLevel;

abstract class ExtensionTestCase extends TestCase
{

protected function processFile(
string $file,
string $expression,
string $type,
array $extensions
): void
{
$broker = $this->createBroker($extensions);
$parser = $this->getParser();
$currentWorkingDirectory = $this->getCurrentWorkingDirectory();
$fileHelper = new FileHelper($currentWorkingDirectory);
$typeSpecifier = $this->createTypeSpecifier(new Standard(), $broker);
/** @var \PHPStan\PhpDoc\PhpDocStringResolver $phpDocStringResolver */
$phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class);
$resolver = new NodeScopeResolver(
$broker,
$parser,
new FileTypeMapper(
$parser,
$phpDocStringResolver,
self::getContainer()->getByType(PhpDocNodeResolver::class),
$this->createMock(Cache::class),
$this->createMock(AnonymousClassNameHelper::class)
),
$fileHelper,
$typeSpecifier,
true,
true,
true,
[],
[]
);
$resolver->setAnalysedFiles([$fileHelper->normalizePath($file)]);

$run = false;
$resolver->processNodes(
$parser->parseFile($file),
$this->createScopeFactory($broker, $typeSpecifier)->create(ScopeContext::create($file)),
function (Node $node, Scope $scope) use ($expression, $type, &$run): void {
if ($node instanceof VirtualNode) {
return;
}
if ((new Standard())->prettyPrint([$node]) !== 'die') {
return;
}
/** @var \PhpParser\Node\Stmt\Expression $expNode */
$expNode = $this->getParser()->parseString(sprintf('<?php %s;', $expression))[0];
self::assertSame($type, $scope->getType($expNode->expr)->describe(VerbosityLevel::typeOnly()));
$run = true;
}
);
self::assertTrue($run);
}

}
37 changes: 37 additions & 0 deletions tests/Type/PHPUnit/MockBuilderTypeExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\PHPUnit;

use ExampleTestCase\BarInterface;
use ExampleTestCase\FooInterface;
use Iterator;
use PHPUnit\Framework\MockObject\MockObject;

class MockBuilderTypeExtensionTest extends ExtensionTestCase
{

/**
* @dataProvider getProvider
* @param string $expression
* @param string $type
*/
public function testMockBuilder(string $expression, string $type): void
{
$this->processFile(
__DIR__ . '/data/mock-builder.php',
$expression,
$type,
[new MockBuilderDynamicReturnTypeExtension(), new GetMockBuilderDynamicReturnTypeExtension()]
);
}

/**
* @return Iterator<mixed>
*/
public function getProvider(): Iterator
{
yield ['$simpleInterface', implode('&', [FooInterface::class, MockObject::class])];
yield ['$doubleInterface', implode('&', [BarInterface::class, FooInterface::class, MockObject::class])];
}

}
9 changes: 9 additions & 0 deletions tests/Type/PHPUnit/data/BarInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);


namespace ExampleTestCase;


interface BarInterface
{
}
9 changes: 9 additions & 0 deletions tests/Type/PHPUnit/data/FooInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);


namespace ExampleTestCase;


interface FooInterface
{
}
12 changes: 12 additions & 0 deletions tests/Type/PHPUnit/data/create-mock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

use PHPUnit\Framework\TestCase;

$test = new class () extends TestCase {};

$reflection = new ReflectionObject($test);
$reflection->getMethod('createMock')->setAccessible(true);
$simpleInterface = $test->createMock(\ExampleTestCase\FooInterface::class);
$doubleInterface = $test->createMock([\ExampleTestCase\FooInterface::class, \ExampleTestCase\BarInterface::class]);

die;
10 changes: 10 additions & 0 deletions tests/Type/PHPUnit/data/mock-builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

use PHPUnit\Framework\TestCase;

$test = new class () extends TestCase {};

$simpleInterface = $test->getMockBuilder(\ExampleTestCase\FooInterface::class)->getMock();
$doubleInterface = $test->getMockBuilder([\ExampleTestCase\FooInterface::class, \ExampleTestCase\BarInterface::class])->getMock();

die;

0 comments on commit 29b543f

Please sign in to comment.