From 589408dd145dbfef4b4e7cdbde700b7ec87cc6c8 Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 15 Nov 2017 06:31:59 +0100 Subject: [PATCH 1/8] Increase test coverage for Injector --- test/InjectorTest.php | 119 +++++++++++++++++++ test/TestAsset/Constructor/ManyArguments.php | 26 ++++ test/TestAsset/TypelessDependency.php | 18 +++ 3 files changed, 163 insertions(+) create mode 100644 test/TestAsset/Constructor/ManyArguments.php create mode 100644 test/TestAsset/TypelessDependency.php diff --git a/test/InjectorTest.php b/test/InjectorTest.php index d37a1a77..6027c00d 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -16,6 +16,8 @@ use Zend\Di\Injector; use Zend\Di\Resolver\DependencyResolverInterface; use ZendTest\Di\TestAsset\DependencyTree as TreeTestAsset; +use Zend\Di\Definition\DefinitionInterface; +use Zend\Di\Resolver\TypeInjection; /** * @coversDefaultClass Zend\Di\Injector @@ -341,4 +343,121 @@ public function testComplexDeepDependencyConfiguration() $this->assertSame($expected1, $result->result->result->optionalResult); $this->assertSame($expected2, $result->result2->result->optionalResult); } + + public function testCreateInstanceWithoutUnknownClassThrowsException() + { + $this->expectException(Exception\ClassNotFoundException::class); + (new Injector())->create('Unknown.Alias.Should.Fail'); + } + + public function testKnownButInexistentClassThrowsException() + { + $definition = $this->getMockBuilder(DefinitionInterface::class) + ->getMockForAbstractClass(); + + $definition->expects($this->any()) + ->method('hasClass') + ->willReturn(true); + + $this->expectException(Exception\ClassNotFoundException::class); + (new Injector(null, null, $definition))->create('ZendTest\Di\TestAsset\No\Such\Class'); + } + + public function provideUnexpectedResolverValues() + { + return [ + [ 'string value' ], + [ true ], + [ null ], + [ new \stdClass() ] + ]; + } + + /** + * @dataProvider provideUnexpectedResolverValues + */ + public function testUnexpectedResolverResultThrowsException($unexpectedValue) + { + $resolver = $this->getMockBuilder(DependencyResolverInterface::class)->getMockForAbstractClass(); + $resolver->expects($this->atLeastOnce()) + ->method('resolveParameters') + ->willReturn([$unexpectedValue]); + + $this->expectException(Exception\UnexpectedValueException::class); + + $injector = new Injector(null, null, null, $resolver); + $injector->create(TestAsset\TypelessDependency::class); + } + + public function provideContainerTypeNames() + { + return [ + [ContainerInterface::class], + ['Interop\Container\ContainerInterface'] + ]; + } + + /** + * @dataProvider provideContainerTypeNames + */ + public function testContainerItselfIsInjectedIfHasReturnsFalse($typeName) + { + $resolver = $this->getMockBuilder(DependencyResolverInterface::class)->getMockForAbstractClass(); + $container = $this->getMockBuilder(ContainerInterface::class)->getMockForAbstractClass(); + $resolver->expects($this->atLeastOnce()) + ->method('resolveParameters') + ->willReturn([new TypeInjection($typeName)]); + + $container->method('has')->willReturn(false); + + $injector = new Injector(null, $container, null, $resolver); + $result = $injector->create(TestAsset\TypelessDependency::class); + + $this->assertInstanceOf(TestAsset\TypelessDependency::class, $result); + $this->assertSame($container, $result->result); + } + + public function testTypeUnavailableInContainerThrowsException() + { + $resolver = $this->getMockBuilder(DependencyResolverInterface::class)->getMockForAbstractClass(); + $container = $this->getMockBuilder(ContainerInterface::class)->getMockForAbstractClass(); + $resolver->expects($this->atLeastOnce()) + ->method('resolveParameters') + ->willReturn([new TypeInjection(TestAsset\A::class)]); + + $container->method('has')->willReturn(false); + + $this->expectException(Exception\UndefinedReferenceException::class); + + $injector = new Injector(null, $container, null, $resolver); + $injector->create(TestAsset\TypelessDependency::class); + } + + public function provideManyArguments() + { + return [ + [[ + 'a' => 'a', + 'b' => 'something', + 'c' => true + ]], + [[ + 'a' => 'a', + 'b' => 'something', + 'c' => true, + 'd' => 8, + 'e' => new \stdClass(), + 'f' => false + ]], + ]; + } + + /** + * @dataProvider provideManyArguments + */ + public function testConstructionWithManyParameters(array $parameters) + { + $result = (new Injector())->create(TestAsset\Constructor\ManyArguments::class, $parameters); + $this->assertEquals($parameters, $result->result); + } } diff --git a/test/TestAsset/Constructor/ManyArguments.php b/test/TestAsset/Constructor/ManyArguments.php new file mode 100644 index 00000000..a929c353 --- /dev/null +++ b/test/TestAsset/Constructor/ManyArguments.php @@ -0,0 +1,26 @@ +result = array_filter(compact('a', 'b', 'c', 'd', 'e', 'f'), function($value) { + return ($value !== null); + }); + } +} diff --git a/test/TestAsset/TypelessDependency.php b/test/TestAsset/TypelessDependency.php new file mode 100644 index 00000000..16ddf528 --- /dev/null +++ b/test/TestAsset/TypelessDependency.php @@ -0,0 +1,18 @@ +result = $value; + } +} From 3f9f54cd5a7ebccfaf39ef4b85f7ce263d3ad9aa Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 15 Nov 2017 06:33:27 +0100 Subject: [PATCH 2/8] Stylefix --- test/TestAsset/Constructor/ManyArguments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TestAsset/Constructor/ManyArguments.php b/test/TestAsset/Constructor/ManyArguments.php index a929c353..f261d3d4 100644 --- a/test/TestAsset/Constructor/ManyArguments.php +++ b/test/TestAsset/Constructor/ManyArguments.php @@ -19,7 +19,7 @@ public function __construct( $e = null, $f = null ) { - $this->result = array_filter(compact('a', 'b', 'c', 'd', 'e', 'f'), function($value) { + $this->result = array_filter(compact('a', 'b', 'c', 'd', 'e', 'f'), function ($value) { return ($value !== null); }); } From c43ff6f56acad6acd91917914a0a567c11efebb3 Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 15 Nov 2017 06:51:37 +0100 Subject: [PATCH 3/8] Increase test coverage for Injector --- test/InjectorTest.php | 36 +++++++++++-------- test/TestAsset/Constructor/ThreeArguments.php | 21 +++++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 test/TestAsset/Constructor/ThreeArguments.php diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 6027c00d..ecc76439 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -436,28 +436,34 @@ public function testTypeUnavailableInContainerThrowsException() public function provideManyArguments() { return [ - [[ - 'a' => 'a', - 'b' => 'something', - 'c' => true - ]], - [[ - 'a' => 'a', - 'b' => 'something', - 'c' => true, - 'd' => 8, - 'e' => new \stdClass(), - 'f' => false - ]], + [ + TestAsset\Constructor\ThreeArguments::class, + [ + 'a' => 'a', + 'b' => 'something', + 'c' => true + ], + ], + [ + TestAsset\Constructor\ManyArguments::class, + [ + 'a' => 'a', + 'b' => 'something', + 'c' => true, + 'd' => 8, + 'e' => new \stdClass(), + 'f' => false + ], + ], ]; } /** * @dataProvider provideManyArguments */ - public function testConstructionWithManyParameters(array $parameters) + public function testConstructionWithManyParameters(string $class, array $parameters) { - $result = (new Injector())->create(TestAsset\Constructor\ManyArguments::class, $parameters); + $result = (new Injector())->create($class, $parameters); $this->assertEquals($parameters, $result->result); } } diff --git a/test/TestAsset/Constructor/ThreeArguments.php b/test/TestAsset/Constructor/ThreeArguments.php new file mode 100644 index 00000000..23ef52db --- /dev/null +++ b/test/TestAsset/Constructor/ThreeArguments.php @@ -0,0 +1,21 @@ +result = compact('a', 'b', 'c'); + } +} From 7025939cde1a605341c1fd62b27bc47e63948663 Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 15 Nov 2017 12:38:36 +0100 Subject: [PATCH 4/8] Add missing resolver test cases --- test/Resolver/DependencyResolverTest.php | 318 ++++++++++++++++++ .../Pseudotypes/CallableImplementation.php | 15 + .../IteratorAggregateImplementation.php | 15 + .../Pseudotypes/IteratorImplementation.php | 31 ++ 4 files changed, 379 insertions(+) create mode 100644 test/TestAsset/Pseudotypes/CallableImplementation.php create mode 100644 test/TestAsset/Pseudotypes/IteratorAggregateImplementation.php create mode 100644 test/TestAsset/Pseudotypes/IteratorImplementation.php diff --git a/test/Resolver/DependencyResolverTest.php b/test/Resolver/DependencyResolverTest.php index 12d87e1b..393c5755 100644 --- a/test/Resolver/DependencyResolverTest.php +++ b/test/Resolver/DependencyResolverTest.php @@ -58,6 +58,8 @@ private function mockParameter($name, $position, array $options) $mock->method('getType')->willReturn($definition['type']); $mock->method('isBuiltin')->willReturn((bool)$definition['builtin']); $mock->method('isRequired')->willReturn((bool)$definition['required']); + + return $mock; } /** @@ -261,4 +263,320 @@ public function testResolveConfiguredPreference(ConfigInterface $config, $reques $resolver = new DependencyResolver(new RuntimeDefinition(), $config); $this->assertSame($expectedType, $resolver->resolvePreference($requestClass, $context)); } + + public function provideExplicitInjections() + { + return [ + [new TypeInjection(TestAsset\B::class)], + [new ValueInjection(new \stdClass())] + ]; + } + + /** + * @dataProvider provideExplicitInjections + */ + public function testExplexitInjectionInConfigIsUsedWithoutAdditionalTypeChecks($expected) + { + $config = new Config([ + 'types' => [ + TestAsset\RequiresA::class => [ + 'parameters' => [ + 'p' => $expected, + ] + ] + ] + ]); + + $resolver = new DependencyResolver(new RuntimeDefinition(), $config); + $result = $resolver->resolveParameters(TestAsset\RequiresA::class); + $this->assertArrayHasKey('p', $result); + $this->assertSame($expected, $result['p']); + } + + public function provideUnusableParametersData() + { + return [ + [ 'string', 123, true ], + [ 'int', 'non-numeric value', true ], + [ 'bool', 'non boolean string', true ], + [ 'iterable', new \stdClass(), true ], + [ 'callable', new \stdClass(), true ], + [ TestAsset\A::class, new \stdClass(), false ], + ]; + } + + /** + * @dataProvider provideUnusableParametersData + */ + public function testUnusableConfigParametersThrowsException(string $type, $value, bool $builtin = false) + { + $class = uniqid('MockedTestClass'); + $paramName = uniqid('param'); + $config = $this->getMockBuilder(ConfigInterface::class)->getMockForAbstractClass(); + $definition = $this->mockDefintition([ + $class => [ + 'parameters' => [ + $paramName => [ + 'type' => $type, + 'builtin' => $builtin + ], + ] + ] + ]); + + $config->method('isAlias')->willReturn(false); + $config->expects($this->atLeastOnce()) + ->method('getParameters') + ->with($class) + ->willReturn([ + $paramName => $value + ]); + + $resolver = new DependencyResolver($definition, $config); + + $this->expectException(Exception\UnexpectedValueException::class); + $resolver->resolveParameters($class); + } + + public function provideUsableParametersData() + { + return [ + [ 'string', '123', true ], + [ 'int', rand(0, 72649), true ], + [ 'int', (float)rand(0, 72649) / 10.0, true ], + [ 'float', rand(0, 72649), true ], + [ 'float', (float)rand(0, 72649) / 10.0, true ], + + // Accepted by php as well + [ 'int', '123', true ], + [ 'float', '123.78', true ], + + [ 'bool', false, true ], + [ 'bool', true, true ], + [ 'iterable', [], true ], + [ 'iterable', new \ArrayIterator([]), true ], + [ 'iterable', new class implements \IteratorAggregate { + public function getIterator() + { + return new \ArrayIterator([]); + } + }, true ], + [ 'callable', function () { + }, true ], + [ 'callable', 'trim', true ], + [ 'callable', new class { + public function __invoke() + { + } + }, true ], + [ TestAsset\B::class, new TestAsset\ExtendedB(new TestAsset\A()), false ], + [ TestAsset\A::class, new TestAsset\A(), false ], + ]; + } + + /** + * @dataProvider provideUsableParametersData + */ + public function testUsableConfigParametersAreAccepted(string $type, $value, bool $builtin = false) + { + $class = uniqid('MockedTestClass'); + $paramName = uniqid('param'); + $definition = $this->mockDefintition([ + $class => [ + 'parameters' => [ + $paramName => [ + 'type' => $type, + 'builtin' => $builtin + ], + ] + ] + ]); + + $config = new Config([ + 'types' => [ + $class => [ + 'parameters' => [ + $paramName => $value + ] + ] + ] + ]); + + $resolver = new DependencyResolver($definition, $config); + $result = $resolver->resolveParameters($class); + + $this->assertArrayHasKey($paramName, $result); + $this->assertInstanceOf(ValueInjection::class, $result[$paramName]); + $this->assertSame($value, $result[$paramName]->getValue()); + } + + /** + * Use Case: + * + * - A class requires an interface "A". + * - The configuration defines this parameter to inject another interface which extends "A" + * + * In this case the resolver must accept it. + */ + public function testConfiguredExtendedInterfaceParameterSatisfiesRequiredInterfaceType() + { + $class = uniqid('MockedTestClass'); + $paramName = uniqid('param'); + $definition = $this->mockDefintition([ + $class => [ + 'parameters' => [ + $paramName => [ + 'type' => TestAsset\Hierarchy\InterfaceA::class + ], + ] + ] + ]); + + $config = new Config([ + 'types' => [ + $class => [ + 'parameters' => [ + $paramName => TestAsset\Hierarchy\InterfaceC::class + ] + ] + ] + ]); + + $resolver = new DependencyResolver($definition, $config); + $result = $resolver->resolveParameters($class); + + $this->assertArrayHasKey($paramName, $result); + $this->assertInstanceOf(TypeInjection::class, $result[$paramName]); + $this->assertEquals(TestAsset\Hierarchy\InterfaceC::class, $result[$paramName]->getType()); + } + + public function provideIterableClassNames() + { + return [ + [ TestAsset\Pseudotypes\IteratorImplementation::class ], + [ TestAsset\Pseudotypes\IteratorAggregateImplementation::class ], + [ \ArrayObject::class ], + [ \ArrayIterator::class ], + ]; + } + + /** + * Scenario: + * + * - A class requires an iterable + * - The configuration defines this parameter to inject a type that implement Traversable + * + * In this case the resolver must accept it. + * + * @dataProvider provideIterableClassNames + */ + public function testConfiguredTraversableTypeParameterSatisfiesIterable($iterableClassName) + { + $class = TestAsset\IterableDependency::class; + $paramName = 'iterator'; + $definition = new RuntimeDefinition(); + $config = new Config([ + 'types' => [ + $class => [ + 'parameters' => [ + $paramName => $iterableClassName + ] + ] + ] + ]); + + $resolver = new DependencyResolver($definition, $config); + $result = $resolver->resolveParameters($class); + + $this->assertArrayHasKey($paramName, $result); + $this->assertInstanceOf(TypeInjection::class, $result[$paramName]); + $this->assertEquals($iterableClassName, $result[$paramName]->getType()); + } + + /** + * Scenario: + * + * - A class requires a callable + * - The configuration defines this parameter to inject a class that implements __invoke() + * + * In this case the resolver must accept it. + */ + public function testConfiguredInvokableTypeParameterSatisfiesCallable() + { + $class = uniqid('MockedTestClass'); + $paramName = uniqid('param'); + $definition = $this->mockDefintition([ + $class => [ + 'parameters' => [ + $paramName => [ + 'type' => 'callable' + ], + ] + ] + ]); + + $config = new Config([ + 'types' => [ + $class => [ + 'parameters' => [ + $paramName => TestAsset\Pseudotypes\CallableImplementation::class, + ] + ], + 'Callable.Alias' => [ + 'typeOf' => TestAsset\Pseudotypes\CallableImplementation::class + ] + ] + ]); + + $resolver = new DependencyResolver($definition, $config); + $result = $resolver->resolveParameters($class); + + $this->assertArrayHasKey($paramName, $result); + $this->assertInstanceOf(TypeInjection::class, $result[$paramName]); + $this->assertEquals(TestAsset\Pseudotypes\CallableImplementation::class, $result[$paramName]->getType()); + } + + /** + * Scenario: + * + * - A class requires a callable + * - The configuration defines this parameter to inject an alias that + * points to a class which implements __invoke() + * + * In this case the resolver must accept it. + */ + public function testConfiguredInvokableAliasParameterSatisfiesCallable() + { + $class = uniqid('MockedTestClass'); + $paramName = uniqid('param'); + $definition = $this->mockDefintition([ + $class => [ + 'parameters' => [ + $paramName => [ + 'type' => 'callable' + ], + ] + ] + ]); + + $config = new Config([ + 'types' => [ + $class => [ + 'parameters' => [ + $paramName => 'Callable.Alias', + ] + ], + 'Callable.Alias' => [ + 'typeOf' => TestAsset\Pseudotypes\CallableImplementation::class + ] + ] + ]); + + $resolver = new DependencyResolver($definition, $config); + $result = $resolver->resolveParameters($class); + + $this->assertArrayHasKey($paramName, $result); + $this->assertInstanceOf(TypeInjection::class, $result[$paramName]); + $this->assertEquals('Callable.Alias', $result[$paramName]->getType()); + } } diff --git a/test/TestAsset/Pseudotypes/CallableImplementation.php b/test/TestAsset/Pseudotypes/CallableImplementation.php new file mode 100644 index 00000000..9c5afe67 --- /dev/null +++ b/test/TestAsset/Pseudotypes/CallableImplementation.php @@ -0,0 +1,15 @@ + Date: Wed, 15 Nov 2017 12:38:45 +0100 Subject: [PATCH 5/8] Fix failing test cases --- src/Resolver/DependencyResolver.php | 61 +++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/src/Resolver/DependencyResolver.php b/src/Resolver/DependencyResolver.php index 5c1acbaa..668ad179 100644 --- a/src/Resolver/DependencyResolver.php +++ b/src/Resolver/DependencyResolver.php @@ -47,6 +47,13 @@ class DependencyResolver implements DependencyResolverInterface 'array', 'resource', 'callable', + 'iterable' + ]; + + private $gettypeMap = [ + 'boolean' => 'bool', + 'integer' => 'int', + 'double' => 'float' ]; public function __construct(DefinitionInterface $definition, ConfigInterface $config) @@ -97,7 +104,7 @@ private function getConfiguredParameters(string $requestedType) : array // A type configuration may define a parameter should be auto resolved // even it was defined earlier $params = array_filter($params, function ($value) { - return ($value != '*'); + return ($value !== '*'); }); return $params; @@ -139,6 +146,16 @@ private function isUsableType(string $type, string $requiredType) : bool && (! $this->container || $this->container->has($type)); } + /** + * @param mixed $value + * @return string + */ + private function getTypeNameFromValue($value): string + { + $type = gettype($value); + return (isset($this->gettypeMap[$type])) ? $this->gettypeMap[$type] : $type; + } + /** * Check if the given value sadisfies the given type * @@ -159,7 +176,15 @@ private function isValueOf($value, string $type) : bool return (is_array($value) || ($value instanceof Traversable)); } - return ($type == gettype($value)); + $valueType = $this->getTypeNameFromValue($value); + $numerics = ['int', 'float']; + + // PHP accepts float for int and vice versa, as well as numeric string values + if (in_array($type, $numerics)) { + return in_array($valueType, $numerics) || (is_string($value) && is_numeric($value)); + } + + return ($type == $valueType); } private function isBuiltinType(string $type) : bool @@ -176,6 +201,26 @@ public function setContainer(ContainerInterface $container) return $this; } + /** + * @param string $type + * @return bool + */ + private function isCallableType(string $type): bool + { + if ($this->config->isAlias($type)) { + $type = $this->config->getClassForAlias($type); + } + + if (! class_exists($type) && ! interface_exists($type)) { + return false; + } + + $reflection = new \ReflectionClass($type); + + return $reflection->hasMethod('__invoke') && + $reflection->getMethod('__invoke')->isPublic(); + } + /** * Prepare a candidate for injection * @@ -196,10 +241,20 @@ private function prepareInjection($value, ?string $requiredType) : ?AbstractInje return $isAvailableInContainer ? new TypeInjection($value) : new ValueInjection($value); } - if (is_string($value) && ($requiredType != 'string')) { + if (is_string($value) && ! $this->isBuiltinType($requiredType)) { return $this->isUsableType($value, $requiredType) ? new TypeInjection($value) : null; } + // Classes may implement iterable + if (is_string($value) && ($requiredType === 'iterable')) { + return $this->isUsableType($value, 'Traversable') ? new TypeInjection($value) : null; + } + + // Classes may implement callable, but strings could be callable as well + if (is_string($value) && ($requiredType === 'callable') && $this->isCallableType($value)) { + return new TypeInjection($value); + } + return $this->isValueOf($value, $requiredType) ? new ValueInjection($value) : null; } From eea41d153a69e61adb904610113bf2c08e742953 Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 15 Nov 2017 16:07:57 +0100 Subject: [PATCH 6/8] Increase test coverage --- test/ConfigProviderTest.php | 32 ++++++++++++++ test/ConfigTest.php | 74 ++++++++++++++++++++++++++++++++ test/LegacyConfigTest.php | 69 +++++++++++++++++++++++++++++ test/ModuleTest.php | 28 ++++++++++++ test/_files/legacy-configs/a.php | 59 +++++++++++++++++++++++++ 5 files changed, 262 insertions(+) create mode 100644 test/ConfigProviderTest.php create mode 100644 test/LegacyConfigTest.php create mode 100644 test/ModuleTest.php create mode 100644 test/_files/legacy-configs/a.php diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php new file mode 100644 index 00000000..07e19524 --- /dev/null +++ b/test/ConfigProviderTest.php @@ -0,0 +1,32 @@ +assertInternalType(IsType::TYPE_CALLABLE, new ConfigProvider()); + } + + public function testProvidesDependencies() + { + $provider = new ConfigProvider(); + $result = $provider(); + + $this->assertArrayHasKey('dependencies', $result); + $this->assertEquals($provider->getDependencyConfig(), $result['dependencies']); + } +} diff --git a/test/ConfigTest.php b/test/ConfigTest.php index 9b3a47fb..0a3c1fed 100644 --- a/test/ConfigTest.php +++ b/test/ConfigTest.php @@ -9,6 +9,7 @@ use PHPUnit\Framework\TestCase; use Zend\Di\Config; +use Zend\Di\Exception; class ConfigTest extends TestCase { @@ -73,4 +74,77 @@ public function testGetTypePreference() $this->assertNull($config->getTypePreference('B', 'NotDefinedType')); $this->assertNull($config->getTypePreference('NotDefined', 'NotDefinedType')); } + + public function testConstructWithInvalidOptionsThrowsException() + { + $this->expectException(Exception\InvalidArgumentException::class); + new Config(new \stdClass()); + } + + public function testSetParameters() + { + $instance = new Config(); + $expected = [ + 'bar' => 'Baz' + ]; + + $this->assertEmpty($instance->getParameters('Foo')); + $instance->setParameters('Foo', $expected); + $this->assertEquals($expected, $instance->getParameters('Foo')); + } + + public function testSetGlobalTypePreference() + { + $instance = new Config(); + $this->assertNull($instance->getTypePreference('Foo')); + $instance->setTypePreference('Foo', 'Bar'); + $this->assertEquals('Bar', $instance->getTypePreference('Foo')); + } + + public function testSetTypePreferenceForTypeContext() + { + $instance = new Config(); + $this->assertNull($instance->getTypePreference('Foo', 'Baz')); + $instance->setTypePreference('Foo', 'Bar', 'Baz'); + $this->assertEquals('Bar', $instance->getTypePreference('Foo', 'Baz')); + } + + public function provideValidClassNames() + { + return [ + [ TestAsset\A::class ], + [ TestAsset\DummyInterface::class ], + ]; + } + + /** + * @dataProvider provideValidClassNames + */ + public function testSetAlias($className) + { + $instance = new Config(); + + $this->assertFalse($instance->isAlias('Foo.Bar')); + + $instance->setAlias('Foo.Bar', $className); + + $this->assertTrue($instance->isAlias('Foo.Bar')); + $this->assertEquals($className, $instance->getClassForAlias('Foo.Bar')); + } + + public function provideInvalidClassNames() + { + return [ + [ 'Bad.Class.Name.For.PHP' ], + ]; + } + + /** + * @dataProvider provideInvalidClassNames + */ + public function testSetAliasThrowsExceptionForInvalidClass(string $invalidClass) + { + $this->expectException(Exception\ClassNotFoundException::class); + (new Config())->setAlias(uniqid('Some.Alias'), $invalidClass); + } } diff --git a/test/LegacyConfigTest.php b/test/LegacyConfigTest.php new file mode 100644 index 00000000..08cfcc5a --- /dev/null +++ b/test/LegacyConfigTest.php @@ -0,0 +1,69 @@ +getPathname(); + yield [ + $data['config'], + $data['expected'] + ]; + } + } + + /** + * @dataProvider provideMigrationConfigFixtures + */ + public function testLegacyConfigMigration(array $config, array $expected) + { + $instance = new LegacyConfig($config); + $this->assertEquals($expected, $instance->toArray()); + } + + public function testFQParamNamesTriggerDeprectade() + { + $this->expectException(DeprecatedError::class); + + new LegacyConfig([ + 'instance' => [ + 'FooClass' => [ + 'parameters' => [ + 'BarClass:__construct:0' => 'Value for fq param name' + ] + ] + ] + ]); + } + + public function testConstructWithTraversable() + { + $spec = include __DIR__ . '/_files/legacy-configs/a.php'; + $config = new \ArrayIterator($spec['config']); + $instance = new LegacyConfig($config); + + $this->assertEquals($spec['expected'], $instance->toArray()); + } + + public function testConstructWithInvalidConfigThrowsException() + { + $this->expectException(Exception\InvalidArgumentException::class); + new LegacyConfig(new \stdClass()); + } +} diff --git a/test/ModuleTest.php b/test/ModuleTest.php new file mode 100644 index 00000000..9a30d2c9 --- /dev/null +++ b/test/ModuleTest.php @@ -0,0 +1,28 @@ +getConfig(); + $this->assertArrayHasKey('service_manager', $config); + $this->assertEquals($configProvider->getDependencyConfig(), $config['service_manager']); + } +} diff --git a/test/_files/legacy-configs/a.php b/test/_files/legacy-configs/a.php new file mode 100644 index 00000000..fcfed818 --- /dev/null +++ b/test/_files/legacy-configs/a.php @@ -0,0 +1,59 @@ + [ + // This should be silently ignored + 'definition' => [ + 'compiler' => [], + 'runtime' => [], + ], + + 'instance' => [ + 'aliases' => [ + 'A.Alias' => TestAsset\A::class, + 'B.Alias' => TestAsset\B::class, + ], + 'preferences' => [ + TestAsset\DummyInterface::class => [ + TestAsset\B::class, + TestAsset\A::class + ], + TestAsset\B::class => TestAsset\ExtendedB::class, + ], + TestAsset\ExtendedB::class => [ + 'shared' => true, + 'parameters' => [ + 'a' => TestAsset\A::class, + 'b' => 'String value for "b"' + ], + ] + ] + ], + 'expected' => [ + 'preferences' => [ + TestAsset\DummyInterface::class => TestAsset\A::class, + TestAsset\B::class => TestAsset\ExtendedB::class, + ], + 'types' => [ + 'A.Alias' => [ + 'typeOf' => TestAsset\A::class, + ], + 'B.Alias' => [ + 'typeOf' => TestAsset\B::class, + ], + TestAsset\ExtendedB::class => [ + 'parameters' => [ + 'a' => TestAsset\A::class, + 'b' => 'String value for "b"' + ] + ] + ] + ], +]; From 3cef4cd3c73004e0a5add7bb033c64dfc5716528 Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 15 Nov 2017 16:31:12 +0100 Subject: [PATCH 7/8] Increase test coverage --- test/ConfigTest.php | 3 ++ test/Container/InjectorFactoryTest.php | 66 ++++++++++++++++++++++++++ test/LegacyConfigTest.php | 3 ++ 3 files changed, 72 insertions(+) create mode 100644 test/Container/InjectorFactoryTest.php diff --git a/test/ConfigTest.php b/test/ConfigTest.php index 0a3c1fed..3d4de986 100644 --- a/test/ConfigTest.php +++ b/test/ConfigTest.php @@ -11,6 +11,9 @@ use Zend\Di\Config; use Zend\Di\Exception; +/** + * @coversDefaultClass Zend\Di\Config + */ class ConfigTest extends TestCase { /** diff --git a/test/Container/InjectorFactoryTest.php b/test/Container/InjectorFactoryTest.php new file mode 100644 index 00000000..d5217177 --- /dev/null +++ b/test/Container/InjectorFactoryTest.php @@ -0,0 +1,66 @@ +assertInternalType(IsType::TYPE_CALLABLE, new InjectorFactory()); + } + + public function testCreateWillReturnAnInjectorInstance() + { + $container = $this->getMockBuilder(ContainerInterface::class)->getMockForAbstractClass(); + $result = (new InjectorFactory())->create($container); + + $this->assertInstanceOf(InjectorInterface::class, $result); + } + + public function testInvokeWillReturnAnInjectorInstance() + { + $container = $this->getMockBuilder(ContainerInterface::class)->getMockForAbstractClass(); + $factory = new InjectorFactory(); + $result = $factory($container); + + $this->assertInstanceOf(InjectorInterface::class, $result); + } + + public function testUsesConfigServiceFromContainer() + { + $container = $this->getMockBuilder(ContainerInterface::class)->getMockForAbstractClass(); + $configMock = $this->getMockBuilder(ConfigInterface::class)->getMockForAbstractClass(); + $container->expects($this->atLeastOnce()) + ->method('has') + ->with(ConfigInterface::class) + ->willReturn(true); + + $container->expects($this->atLeastOnce()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($configMock); + + $injector = (new InjectorFactory())->create($container); + + $reflection = new \ReflectionObject($injector); + $property = $reflection->getProperty('config'); + + $property->setAccessible(true); + $this->assertSame($configMock, $property->getValue($injector)); + } +} diff --git a/test/LegacyConfigTest.php b/test/LegacyConfigTest.php index 08cfcc5a..af94dd5e 100644 --- a/test/LegacyConfigTest.php +++ b/test/LegacyConfigTest.php @@ -12,6 +12,9 @@ use Zend\Di\LegacyConfig; use Zend\Di\Exception; +/** + * @coversDefaultClass Zend\Di\LegacyConfig + */ class LegacyConfigTest extends TestCase { public function provideMigrationConfigFixtures() From 58f5d0d907545148348c2e6a01a50f01d91298df Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 15 Nov 2017 16:31:39 +0100 Subject: [PATCH 8/8] Fix failing tests for injector factory --- src/Container/InjectorFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Container/InjectorFactory.php b/src/Container/InjectorFactory.php index 91dc6d2a..7cb608b8 100644 --- a/src/Container/InjectorFactory.php +++ b/src/Container/InjectorFactory.php @@ -37,7 +37,7 @@ private function createConfig(ContainerInterface $container) : ConfigInterface public function create(ContainerInterface $container) : InjectorInterface { $config = $this->createConfig($container); - return new Injector($config, null, null, $this); + return new Injector($config, $container); } /**