Testing the container — unit tests for resolution logic
Concept
Testing the container verifies that your bindings resolve correctly, that interfaces map to the right implementations, that singletons return the same instance, and that circular dependencies are detected. Container tests are unit tests — they test the wiring of your framework, not business logic.
What to test:
- Concrete class auto-resolution with dependencies.
- Interface-to-implementation binding resolution.
- Singleton behavior (same instance on repeated resolution).
- Contextual binding (different implementations for different classes).
- Exception thrown on unresolvable dependency.
- Circular dependency detection.
Test approach: Instantiate your Container directly (or use a clean instance). Bind, then resolve. Assert the resolved object is the expected type. For singletons, assert === identity (same instance).
Avoiding global state: Each test should use a fresh container instance. Don't test against the global container — it may have bindings from other test runs.
PHPUnit for container unit tests: Since the container doesn't need HTTP or database, use PHPUnit\Framework\TestCase (not Laravel's test case). Pure unit test — fast, no app boot.
Testing auto-wiring: Pass a class with constructor dependencies to make(). The container should resolve the dependency tree automatically.
Code Example
<?php
namespace Tests\Unit\Container;
use PHPUnit\Framework\TestCase;
use Framework\Container\Container;
// Sample classes for testing
interface LoggerInterface { public function log(string $message): void; }
class FileLogger implements LoggerInterface { public function log(string $message): void {} }
class DatabaseLogger implements LoggerInterface { public function log(string $message): void {} }
class Service {
public function __construct(public readonly LoggerInterface $logger) {}
}
class ContainerTest extends TestCase
{
private Container $container;
protected function setUp(): void
{
$this->container = new Container();
}
public function test_resolves_concrete_class_automatically(): void
{
$logger = $this->container->make(FileLogger::class);
$this->assertInstanceOf(FileLogger::class, $logger);
}
public function test_resolves_interface_to_bound_implementation(): void
{
$this->container->bind(LoggerInterface::class, FileLogger::class);
$logger = $this->container->make(LoggerInterface::class);
$this->assertInstanceOf(FileLogger::class, $logger);
}
public function test_singleton_returns_same_instance(): void
{
$this->container->singleton(LoggerInterface::class, FileLogger::class);
$first = $this->container->make(LoggerInterface::class);
$second = $this->container->make(LoggerInterface::class);
$this->assertSame($first, $second); // === identity
}
public function test_bind_returns_new_instance_each_time(): void
{
$this->container->bind(LoggerInterface::class, FileLogger::class);
$first = $this->container->make(LoggerInterface::class);
$second = $this->container->make(LoggerInterface::class);
$this->assertNotSame($first, $second);
}
public function test_auto_wires_constructor_dependencies(): void
{
$this->container->bind(LoggerInterface::class, FileLogger::class);
$service = $this->container->make(Service::class);
$this->assertInstanceOf(Service::class, $service);
$this->assertInstanceOf(FileLogger::class, $service->logger);
}
public function test_throws_on_unresolvable_interface(): void
{
$this->expectException(\Framework\Container\BindingResolutionException::class);
$this->container->make(LoggerInterface::class); // no binding
}
public function test_closure_binding_is_called(): void
{
$called = false;
$this->container->bind(LoggerInterface::class, function() use (&$called) {
$called = true;
return new FileLogger();
});
$this->container->make(LoggerInterface::class);
$this->assertTrue($called);
}
}