WPDI makes testing easy through constructor injection - mock dependencies and test in isolation.
class Payment_Processor_Test extends WP_UnitTestCase {
public function test_processes_valid_payment(): void {
$validator = $this->createMock( Payment_Validator::class );
$validator->method( 'validate' )->willReturn( true );
$processor = new Payment_Processor( $validator );
$result = $processor->process_payment( 100.00 );
$this->assertTrue( $result );
}
public function test_rejects_invalid_payment(): void {
$validator = $this->createMock( Payment_Validator::class );
$validator->method( 'validate' )->willReturn( false );
$processor = new Payment_Processor( $validator );
$this->assertFalse( $processor->process_payment( -10.00 ) );
}
}Create test implementations:
class Test_Logger implements Logger_Interface {
public array $messages = array();
public function log( string $message ): void {
$this->messages[] = $message;
}
}
class Service_Test extends WP_UnitTestCase {
public function test_logs_messages(): void {
$logger = new Test_Logger();
$service = new My_Service( $logger );
$service->do_something();
$this->assertCount( 1, $logger->messages );
}
}Test with the full container:
class Plugin_Integration_Test extends WP_UnitTestCase {
private WPDI\Container $container;
public function setUp(): void {
parent::setUp();
$this->container = new WPDI\Container();
// Override with test implementations (simple and contextual)
$this->container->load_config( array(
Logger_Interface::class => Test_Logger::class,
API_Client::class => Mock_API_Client::class,
Cache_Interface::class => array(
'$db_cache' => Test_DB_Cache::class,
'default' => Test_File_Cache::class,
),
) );
}
public function test_full_workflow(): void {
$app = $this->container->get( Payment_Application::class );
$app->run();
$this->assertTrue( has_action( 'woocommerce_payment_complete' ) );
}
}class Hooks_Test extends WP_UnitTestCase {
public function test_registers_hooks(): void {
$processor = $this->createMock( Payment_Processor::class );
$hooks = new Payment_Hooks( $processor );
$hooks->init();
$this->assertTrue( has_action( 'woocommerce_payment_complete' ) );
}
public function test_hook_triggers_callback(): void {
$processor = $this->createMock( Payment_Processor::class );
$processor->expects( $this->once() )
->method( 'handle' )
->with( 123 );
$hooks = new Payment_Hooks( $processor );
$hooks->init();
do_action( 'woocommerce_payment_complete', 123 );
}
}WPDI's singleton cache is shared across all Container instances (see Multi-Plugin Patterns). When testing with Scope, call clear() in tearDown() to reset both the booted instance and the shared singleton pool:
protected function tearDown(): void {
parent::tearDown();
My_Plugin::clear(); // Clears booted state + shared singleton pool
}When testing with Container directly, call clear() on the container instance:
protected function tearDown(): void {
parent::tearDown();
$this->container->clear(); // Clears bindings + shared singleton pool
}tests/
├── unit/ # Fast, isolated tests
├── integration/ # Full container tests
├── mocks/ # Test doubles
└── bootstrap.php
# Run all tests
ddev composer test
# Run specific test
ddev exec vendor/bin/phpunit tests/unit/Payment_Test.php