From dd1f47fc1ea59e4e6cd67bf637980f3aa9a46441 Mon Sep 17 00:00:00 2001 From: PrinsFrank <25006490+PrinsFrank@users.noreply.github.com> Date: Sun, 7 Jul 2024 20:59:09 +0200 Subject: [PATCH] Add a bunch of UTs --- src/Container.php | 2 +- src/Definition/DefinitionSet.php | 2 +- src/Definition/Item/AbstractConcrete.php | 5 +- src/Definition/Item/Concrete.php | 5 +- src/Definition/Item/Singleton.php | 5 +- src/Exception/InvalidMethodException.php | 2 +- src/Exception/MissingDefinitionException.php | 2 +- src/Resolver/ParameterResolver.php | 2 +- src/ServiceProvider/ContainerProvider.php | 2 +- tests/Unit/Definition/DefinitionSetTest.php | 19 +++++ .../Definition/Item/AbstractConcreteTest.php | 74 +++++++++++++++++ tests/Unit/Definition/Item/ConcreteTest.php | 81 +++++++++++++++++++ tests/Unit/Definition/Item/SingletonTest.php | 81 +++++++++++++++++++ .../ServiceProvider/ContainerProviderTest.php | 33 ++++++++ 14 files changed, 303 insertions(+), 12 deletions(-) create mode 100644 tests/Unit/Definition/DefinitionSetTest.php create mode 100644 tests/Unit/Definition/Item/AbstractConcreteTest.php create mode 100644 tests/Unit/Definition/Item/ConcreteTest.php create mode 100644 tests/Unit/Definition/Item/SingletonTest.php create mode 100644 tests/Unit/ServiceProvider/ContainerProviderTest.php diff --git a/src/Container.php b/src/Container.php index 4f5fb4f..c490d06 100644 --- a/src/Container.php +++ b/src/Container.php @@ -14,7 +14,7 @@ use PrinsFrank\Container\ServiceProvider\ServiceProviderInterface; use Psr\Container\ContainerInterface; -class Container implements ContainerInterface { +final class Container implements ContainerInterface { /** @var list */ private array $serviceProvider = []; private readonly DefinitionSet $resolvedSet; diff --git a/src/Definition/DefinitionSet.php b/src/Definition/DefinitionSet.php index b30078b..870fcd2 100644 --- a/src/Definition/DefinitionSet.php +++ b/src/Definition/DefinitionSet.php @@ -9,7 +9,7 @@ use PrinsFrank\Container\Exception\ShouldNotHappenException; use PrinsFrank\Container\Resolver\ParameterResolver; -final class DefinitionSet { +class DefinitionSet { /** @var list> */ private array $definitions = []; diff --git a/src/Definition/Item/AbstractConcrete.php b/src/Definition/Item/AbstractConcrete.php index 8353f5e..a16c81a 100644 --- a/src/Definition/Item/AbstractConcrete.php +++ b/src/Definition/Item/AbstractConcrete.php @@ -13,12 +13,13 @@ use PrinsFrank\Container\Exception\UnresolvableException; use PrinsFrank\Container\Resolver\ParameterResolver; use ReflectionClass; +use stdClass; /** * @template T of object * @implements Definition */ -final readonly class AbstractConcrete implements Definition { +readonly class AbstractConcrete implements Definition { /** * @param class-string $identifier * @param Closure(): T|Closure(): null $new @@ -43,7 +44,7 @@ public function isFor(string $identifier): bool { public function get(Container $container, ParameterResolver $parameterResolver): ?object { $resolved = ($this->new)(...$parameterResolver->resolveParamsForClosure($this->new)); if ($resolved !== null && $resolved instanceof $this->identifier === false) { - throw new InvalidServiceProviderException(sprintf('Closure returned type "%s" instead of concrete for "%s"', gettype($resolved), $this->identifier)); + throw new InvalidServiceProviderException(sprintf('Closure returned type "%s" instead of concrete for "%s"', $resolved instanceof stdClass ? get_class($resolved) : gettype($resolved), $this->identifier)); } return $resolved; diff --git a/src/Definition/Item/Concrete.php b/src/Definition/Item/Concrete.php index 85517a4..77522c5 100644 --- a/src/Definition/Item/Concrete.php +++ b/src/Definition/Item/Concrete.php @@ -13,12 +13,13 @@ use PrinsFrank\Container\Exception\UnresolvableException; use PrinsFrank\Container\Resolver\ParameterResolver; use ReflectionClass; +use stdClass; /** * @template T of object * @implements Definition */ -final readonly class Concrete implements Definition { +readonly class Concrete implements Definition { /** * @param class-string $identifier * @param Closure(): T|Closure(): null $new @@ -43,7 +44,7 @@ public function isFor(string $identifier): bool { public function get(Container $container, ParameterResolver $parameterResolver): ?object { $resolved = ($this->new)(...$parameterResolver->resolveParamsForClosure($this->new)); if ($resolved !== null && $resolved instanceof $this->identifier === false) { - throw new InvalidServiceProviderException(sprintf('Closure returned type "%s" instead of "%s"', gettype($resolved), $this->identifier)); + throw new InvalidServiceProviderException(sprintf('Closure returned type "%s" instead of "%s"', $resolved instanceof stdClass ? get_class($resolved) : gettype($resolved), $this->identifier)); } return $resolved; diff --git a/src/Definition/Item/Singleton.php b/src/Definition/Item/Singleton.php index 0bf78e2..5c1d8ab 100644 --- a/src/Definition/Item/Singleton.php +++ b/src/Definition/Item/Singleton.php @@ -13,12 +13,13 @@ use PrinsFrank\Container\Exception\UnresolvableException; use PrinsFrank\Container\Resolver\ParameterResolver; use ReflectionClass; +use stdClass; /** * @template T of object * @implements Definition */ -final readonly class Singleton implements Definition { +readonly class Singleton implements Definition { /** @var T|null */ private ?object $instance; // @phpstan-ignore property.uninitializedReadonly @@ -47,7 +48,7 @@ public function get(Container $container, ParameterResolver $parameterResolver): if (isset($this->instance) === false) { $resolved = ($this->new)(...$parameterResolver->resolveParamsForClosure($this->new)); if ($resolved !== null && $resolved instanceof $this->identifier === false) { - throw new InvalidServiceProviderException(sprintf('Closure returned type "%s" instead of "%s"', gettype($resolved), $this->identifier)); + throw new InvalidServiceProviderException(sprintf('Closure returned type "%s" instead of "%s"', $resolved instanceof stdClass ? get_class($resolved) : gettype($resolved), $this->identifier)); } $this->instance = $resolved; // @phpstan-ignore property.readOnlyAssignNotInConstructor diff --git a/src/Exception/InvalidMethodException.php b/src/Exception/InvalidMethodException.php index 7b0fbd2..9fedd41 100644 --- a/src/Exception/InvalidMethodException.php +++ b/src/Exception/InvalidMethodException.php @@ -2,5 +2,5 @@ namespace PrinsFrank\Container\Exception; -class InvalidMethodException extends ContainerException { +final class InvalidMethodException extends ContainerException { } diff --git a/src/Exception/MissingDefinitionException.php b/src/Exception/MissingDefinitionException.php index 01fb947..b9658b1 100644 --- a/src/Exception/MissingDefinitionException.php +++ b/src/Exception/MissingDefinitionException.php @@ -2,5 +2,5 @@ namespace PrinsFrank\Container\Exception; -class MissingDefinitionException extends ContainerException { +final class MissingDefinitionException extends ContainerException { } diff --git a/src/Resolver/ParameterResolver.php b/src/Resolver/ParameterResolver.php index fb8b9ad..d02eb70 100644 --- a/src/Resolver/ParameterResolver.php +++ b/src/Resolver/ParameterResolver.php @@ -12,7 +12,7 @@ use ReflectionMethod; use ReflectionNamedType; -class ParameterResolver { +final class ParameterResolver { public function __construct( private readonly Container $container, ) { diff --git a/src/ServiceProvider/ContainerProvider.php b/src/ServiceProvider/ContainerProvider.php index ef7f21e..1a2432d 100644 --- a/src/ServiceProvider/ContainerProvider.php +++ b/src/ServiceProvider/ContainerProvider.php @@ -8,7 +8,7 @@ use PrinsFrank\Container\Definition\Item\Singleton; use PrinsFrank\Container\Exception\InvalidArgumentException; -class ContainerProvider implements ServiceProviderInterface { +final class ContainerProvider implements ServiceProviderInterface { #[Override] public function provides(string $identifier): bool { return $identifier === Container::class; diff --git a/tests/Unit/Definition/DefinitionSetTest.php b/tests/Unit/Definition/DefinitionSetTest.php new file mode 100644 index 0000000..5245eca --- /dev/null +++ b/tests/Unit/Definition/DefinitionSetTest.php @@ -0,0 +1,19 @@ +forContainer); + static::assertSame($parameterResolver, $definitionSet->parameterResolver); + } +} diff --git a/tests/Unit/Definition/Item/AbstractConcreteTest.php b/tests/Unit/Definition/Item/AbstractConcreteTest.php new file mode 100644 index 0000000..ee93d02 --- /dev/null +++ b/tests/Unit/Definition/Item/AbstractConcreteTest.php @@ -0,0 +1,74 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Argument $identifier is expected to be a class-string for an interface or abstract class'); + new AbstractConcrete(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => null); + } + + /** @throws InvalidArgumentException */ + public function testIsFor(): void { + $abstractConcrete = new AbstractConcrete(InterfaceA::class, fn () => null); + + static::assertTrue($abstractConcrete->isFor(InterfaceA::class)); + static::assertFalse($abstractConcrete->isFor(AbstractBImplementsInterfaceA::class)); + static::assertFalse($abstractConcrete->isFor(ConcreteCExtendsAbstractBImplementsInterfaceA::class)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveThrowsExceptionWhenClosureReturnsInvalidType(): void { + /** @phpstan-ignore argument.type */ + $abstractConcrete = new AbstractConcrete(InterfaceA::class, fn () => 42); + + $this->expectException(InvalidServiceProviderException::class); + $this->expectExceptionMessage('Closure returned type "integer" instead of concrete for "' . InterfaceA::class . '"'); + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveThrowsExceptionWhenClosureReturnsInvalidClassType(): void { + $abstractConcrete = new AbstractConcrete(InterfaceA::class, fn () => new stdClass()); + + $this->expectException(InvalidServiceProviderException::class); + $this->expectExceptionMessage('Closure returned type "stdClass" instead of concrete for "' . InterfaceA::class . '"'); + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolve(): void { + $concrete = new ConcreteCExtendsAbstractBImplementsInterfaceA(); + $abstractConcrete = new AbstractConcrete(InterfaceA::class, fn () => $concrete); + + static::assertSame( + $concrete, + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)), + ); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveAllowsNullValue(): void { + $abstractConcrete = new AbstractConcrete(InterfaceA::class, fn () => null); + + static::assertNull($abstractConcrete->get($container = new Container(), new ParameterResolver($container))); + } +} diff --git a/tests/Unit/Definition/Item/ConcreteTest.php b/tests/Unit/Definition/Item/ConcreteTest.php new file mode 100644 index 0000000..81c9805 --- /dev/null +++ b/tests/Unit/Definition/Item/ConcreteTest.php @@ -0,0 +1,81 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Argument $identifier is expected to be a class-string for a concrete class'); + new Concrete(AbstractBImplementsInterfaceA::class, fn () => null); + } + + /** @throws InvalidArgumentException */ + public function testIsForThrowsExceptionOnInterface(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Argument $identifier is expected to be a class-string for a concrete class'); + new Concrete(InterfaceA::class, fn () => null); + } + + /** @throws InvalidArgumentException */ + public function testIsFor(): void { + $abstractConcrete = new Concrete(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => null); + + static::assertFalse($abstractConcrete->isFor(InterfaceA::class)); + static::assertFalse($abstractConcrete->isFor(AbstractBImplementsInterfaceA::class)); + static::assertTrue($abstractConcrete->isFor(ConcreteCExtendsAbstractBImplementsInterfaceA::class)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveThrowsExceptionWhenClosureReturnsInvalidType(): void { + /** @phpstan-ignore argument.type */ + $abstractConcrete = new Concrete(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => 42); + + $this->expectException(InvalidServiceProviderException::class); + $this->expectExceptionMessage('Closure returned type "integer" instead of "' . ConcreteCExtendsAbstractBImplementsInterfaceA::class . '"'); + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveThrowsExceptionWhenClosureReturnsInvalidClassType(): void { + $abstractConcrete = new Concrete(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => new stdClass()); + + $this->expectException(InvalidServiceProviderException::class); + $this->expectExceptionMessage('Closure returned type "stdClass" instead of "' . ConcreteCExtendsAbstractBImplementsInterfaceA::class . '"'); + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolve(): void { + $concrete = new ConcreteCExtendsAbstractBImplementsInterfaceA(); + $abstractConcrete = new Concrete(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => $concrete); + + static::assertSame( + $concrete, + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)), + ); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveAllowsNullValue(): void { + $abstractConcrete = new Concrete(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => null); + + static::assertNull($abstractConcrete->get($container = new Container(), new ParameterResolver($container))); + } +} diff --git a/tests/Unit/Definition/Item/SingletonTest.php b/tests/Unit/Definition/Item/SingletonTest.php new file mode 100644 index 0000000..904cdef --- /dev/null +++ b/tests/Unit/Definition/Item/SingletonTest.php @@ -0,0 +1,81 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Argument $identifier is expected to be a class-string for a concrete class'); + new Singleton(AbstractBImplementsInterfaceA::class, fn () => null); + } + + /** @throws InvalidArgumentException */ + public function testIsForThrowsExceptionOnInterface(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Argument $identifier is expected to be a class-string for a concrete class'); + new Singleton(InterfaceA::class, fn () => null); + } + + /** @throws InvalidArgumentException */ + public function testIsFor(): void { + $abstractConcrete = new Singleton(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => null); + + static::assertFalse($abstractConcrete->isFor(InterfaceA::class)); + static::assertFalse($abstractConcrete->isFor(AbstractBImplementsInterfaceA::class)); + static::assertTrue($abstractConcrete->isFor(ConcreteCExtendsAbstractBImplementsInterfaceA::class)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveThrowsExceptionWhenClosureReturnsInvalidType(): void { + /** @phpstan-ignore argument.type */ + $abstractConcrete = new Singleton(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => 42); + + $this->expectException(InvalidServiceProviderException::class); + $this->expectExceptionMessage('Closure returned type "integer" instead of "' . ConcreteCExtendsAbstractBImplementsInterfaceA::class . '"'); + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveThrowsExceptionWhenClosureReturnsInvalidClassType(): void { + $abstractConcrete = new Singleton(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => new stdClass()); + + $this->expectException(InvalidServiceProviderException::class); + $this->expectExceptionMessage('Closure returned type "stdClass" instead of "' . ConcreteCExtendsAbstractBImplementsInterfaceA::class . '"'); + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolve(): void { + $concrete = new ConcreteCExtendsAbstractBImplementsInterfaceA(); + $abstractConcrete = new Singleton(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => $concrete); + + static::assertSame( + $concrete, + $abstractConcrete->get($container = new Container(), new ParameterResolver($container)), + ); + } + + /** @throws InvalidServiceProviderException|MissingDefinitionException|UnresolvableException|InvalidMethodException|InvalidArgumentException */ + public function testResolveAllowsNullValue(): void { + $abstractConcrete = new Singleton(ConcreteCExtendsAbstractBImplementsInterfaceA::class, fn () => null); + + static::assertNull($abstractConcrete->get($container = new Container(), new ParameterResolver($container))); + } +} diff --git a/tests/Unit/ServiceProvider/ContainerProviderTest.php b/tests/Unit/ServiceProvider/ContainerProviderTest.php new file mode 100644 index 0000000..110675b --- /dev/null +++ b/tests/Unit/ServiceProvider/ContainerProviderTest.php @@ -0,0 +1,33 @@ +provides('foo')); + static::assertTrue($containerProvider->provides(Container::class)); + } + + /** @throws InvalidArgumentException|MissingDefinitionException */ + public function testRegister(): void { + $resolvedSet = new DefinitionSet($container = new Container(), new ParameterResolver($container)); + + (new ContainerProvider()) + ->register('foo', $resolvedSet); + + static::assertSame($container, $resolvedSet->get(Container::class, $container)); + } +}