Skip to content

Commit

Permalink
Add a bunch of UTs
Browse files Browse the repository at this point in the history
  • Loading branch information
PrinsFrank committed Jul 7, 2024
1 parent 806292c commit dd1f47f
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use PrinsFrank\Container\ServiceProvider\ServiceProviderInterface;
use Psr\Container\ContainerInterface;

class Container implements ContainerInterface {
final class Container implements ContainerInterface {
/** @var list<ServiceProviderInterface> */
private array $serviceProvider = [];
private readonly DefinitionSet $resolvedSet;
Expand Down
2 changes: 1 addition & 1 deletion src/Definition/DefinitionSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use PrinsFrank\Container\Exception\ShouldNotHappenException;
use PrinsFrank\Container\Resolver\ParameterResolver;

final class DefinitionSet {
class DefinitionSet {
/** @var list<Definition<covariant object>> */
private array $definitions = [];

Expand Down
5 changes: 3 additions & 2 deletions src/Definition/Item/AbstractConcrete.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
use PrinsFrank\Container\Exception\UnresolvableException;
use PrinsFrank\Container\Resolver\ParameterResolver;
use ReflectionClass;
use stdClass;

/**
* @template T of object
* @implements Definition<T>
*/
final readonly class AbstractConcrete implements Definition {
readonly class AbstractConcrete implements Definition {
/**
* @param class-string<T> $identifier
* @param Closure(): T|Closure(): null $new
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/Definition/Item/Concrete.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
use PrinsFrank\Container\Exception\UnresolvableException;
use PrinsFrank\Container\Resolver\ParameterResolver;
use ReflectionClass;
use stdClass;

/**
* @template T of object
* @implements Definition<T>
*/
final readonly class Concrete implements Definition {
readonly class Concrete implements Definition {
/**
* @param class-string<T> $identifier
* @param Closure(): T|Closure(): null $new
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/Definition/Item/Singleton.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
use PrinsFrank\Container\Exception\UnresolvableException;
use PrinsFrank\Container\Resolver\ParameterResolver;
use ReflectionClass;
use stdClass;

/**
* @template T of object
* @implements Definition<T>
*/
final readonly class Singleton implements Definition {
readonly class Singleton implements Definition {
/** @var T|null */
private ?object $instance; // @phpstan-ignore property.uninitializedReadonly

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/InvalidMethodException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

namespace PrinsFrank\Container\Exception;

class InvalidMethodException extends ContainerException {
final class InvalidMethodException extends ContainerException {
}
2 changes: 1 addition & 1 deletion src/Exception/MissingDefinitionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

namespace PrinsFrank\Container\Exception;

class MissingDefinitionException extends ContainerException {
final class MissingDefinitionException extends ContainerException {
}
2 changes: 1 addition & 1 deletion src/Resolver/ParameterResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use ReflectionMethod;
use ReflectionNamedType;

class ParameterResolver {
final class ParameterResolver {
public function __construct(
private readonly Container $container,
) {
Expand Down
2 changes: 1 addition & 1 deletion src/ServiceProvider/ContainerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions tests/Unit/Definition/DefinitionSetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types=1);

namespace PrinsFrank\Container\Tests\Unit\Definition;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use PrinsFrank\Container\Container;
use PrinsFrank\Container\Definition\DefinitionSet;
use PrinsFrank\Container\Resolver\ParameterResolver;

#[CoversClass(DefinitionSet::class)]
class DefinitionSetTest extends TestCase {
public function testConstruct(): void {
$definitionSet = new DefinitionSet($container = new Container(), $parameterResolver = new ParameterResolver($container));

static::assertSame($container, $definitionSet->forContainer);
static::assertSame($parameterResolver, $definitionSet->parameterResolver);
}
}
74 changes: 74 additions & 0 deletions tests/Unit/Definition/Item/AbstractConcreteTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types=1);

namespace PrinsFrank\Container\Tests\Unit\Definition\Item;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use PrinsFrank\Container\Container;
use PrinsFrank\Container\Definition\Item\AbstractConcrete;
use PrinsFrank\Container\Exception\InvalidArgumentException;
use PrinsFrank\Container\Exception\InvalidMethodException;
use PrinsFrank\Container\Exception\InvalidServiceProviderException;
use PrinsFrank\Container\Exception\MissingDefinitionException;
use PrinsFrank\Container\Exception\UnresolvableException;
use PrinsFrank\Container\Resolver\ParameterResolver;
use PrinsFrank\Container\Tests\Fixtures\AbstractBImplementsInterfaceA;
use PrinsFrank\Container\Tests\Fixtures\ConcreteCExtendsAbstractBImplementsInterfaceA;
use PrinsFrank\Container\Tests\Fixtures\InterfaceA;
use stdClass;

#[CoversClass(AbstractConcrete::class)]
class AbstractConcreteTest extends TestCase {
/** @throws InvalidArgumentException */
public function testIsForThrowsExceptionOnNonAbstractClass(): void {
$this->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)));
}
}
81 changes: 81 additions & 0 deletions tests/Unit/Definition/Item/ConcreteTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types=1);

namespace PrinsFrank\Container\Tests\Unit\Definition\Item;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use PrinsFrank\Container\Container;
use PrinsFrank\Container\Definition\Item\Concrete;
use PrinsFrank\Container\Exception\InvalidArgumentException;
use PrinsFrank\Container\Exception\InvalidMethodException;
use PrinsFrank\Container\Exception\InvalidServiceProviderException;
use PrinsFrank\Container\Exception\MissingDefinitionException;
use PrinsFrank\Container\Exception\UnresolvableException;
use PrinsFrank\Container\Resolver\ParameterResolver;
use PrinsFrank\Container\Tests\Fixtures\AbstractBImplementsInterfaceA;
use PrinsFrank\Container\Tests\Fixtures\ConcreteCExtendsAbstractBImplementsInterfaceA;
use PrinsFrank\Container\Tests\Fixtures\InterfaceA;
use stdClass;

#[CoversClass(Concrete::class)]
class ConcreteTest extends TestCase {
/** @throws InvalidArgumentException */
public function testIsForThrowsExceptionOnAbstractClass(): void {
$this->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)));
}
}
81 changes: 81 additions & 0 deletions tests/Unit/Definition/Item/SingletonTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types=1);

namespace PrinsFrank\Container\Tests\Unit\Definition\Item;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use PrinsFrank\Container\Container;
use PrinsFrank\Container\Definition\Item\Singleton;
use PrinsFrank\Container\Exception\InvalidArgumentException;
use PrinsFrank\Container\Exception\InvalidMethodException;
use PrinsFrank\Container\Exception\InvalidServiceProviderException;
use PrinsFrank\Container\Exception\MissingDefinitionException;
use PrinsFrank\Container\Exception\UnresolvableException;
use PrinsFrank\Container\Resolver\ParameterResolver;
use PrinsFrank\Container\Tests\Fixtures\AbstractBImplementsInterfaceA;
use PrinsFrank\Container\Tests\Fixtures\ConcreteCExtendsAbstractBImplementsInterfaceA;
use PrinsFrank\Container\Tests\Fixtures\InterfaceA;
use stdClass;

#[CoversClass(Singleton::class)]
class SingletonTest extends TestCase {
/** @throws InvalidArgumentException */
public function testIsForThrowsExceptionOnAbstractClass(): void {
$this->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)));
}
}
Loading

0 comments on commit dd1f47f

Please sign in to comment.