Improvements to the DI related unit testing infrastructure:

- When ExtendedContainer::replace is used passing an instance of
  an object, the 'init' method will be executed in the instance
  when resolving the replaced class name.
- Added the ExtendedContainer::reset_replacement method, this will
  undo a 'replace' and reset the registration back to its original state.
- Similarly, added the ExtendedContainer::reset_all_replacements method.
This commit is contained in:
Nestor Soriano 2023-06-21 17:39:34 +02:00
parent bc2cba7b81
commit 5d4c07d23c
No known key found for this signature in database
GPG Key ID: 08110F3518C12CAD
6 changed files with 193 additions and 21 deletions

View File

@ -25,15 +25,46 @@ class Definition extends BaseDefinition {
* @return object
*/
protected function resolveClass( string $concrete ) {
$instance = new $concrete();
$this->invokeInit( $instance );
return $instance;
}
/**
* Invoke methods on resolved instance, including 'init'.
*
* @param object $instance The concrete to invoke methods on.
*
* @return object
*/
protected function invokeMethods( $instance ) {
$this->invokeInit( $instance );
parent::invokeMethods( $instance );
return $instance;
}
/**
* Invoke the 'init' method on a resolved object.
*
* Constructor injection causes backwards compatibility problems
* so we will rely on method injection via an internal method.
*
* @param object $instance The resolved object.
* @return void
*/
private function invokeInit( $instance ) {
$resolved = $this->resolveArguments( $this->arguments );
$concrete = new $concrete();
// Constructor injection causes backwards compatibility problems
// so we will rely on method injection via an internal method.
if ( method_exists( $concrete, static::INJECTION_METHOD ) ) {
call_user_func_array( array( $concrete, static::INJECTION_METHOD ), $resolved );
if ( method_exists( $instance, static::INJECTION_METHOD ) ) {
call_user_func_array( array( $instance, static::INJECTION_METHOD ), $resolved );
}
}
return $concrete;
/**
* Forget the cached resolved object, so the next time it's requested
* it will be resolved again.
*/
public function forgetResolved() {
$this->resolved = null;
}
}

View File

@ -23,6 +23,13 @@ class ExtendedContainer extends BaseContainer {
*/
private $woocommerce_namespace = 'Automattic\\WooCommerce\\';
/**
* Holds the original registrations so that 'reset_replacement' can work, keys are class names and values are the original concretes.
*
* @var array
*/
private $original_concretes = array();
/**
* Whitelist of classes that we can register using the container
* despite not belonging to the WooCommerce root namespace.
@ -68,7 +75,7 @@ class ExtendedContainer extends BaseContainer {
}
/**
* Replace an existing registration with a different concrete.
* Replace an existing registration with a different concrete. See also 'reset_replacement' and 'reset_all_replacements'.
*
* @param string $class_name The class name whose definition will be replaced.
* @param mixed $concrete The new concrete (same as "add").
@ -86,17 +93,47 @@ class ExtendedContainer extends BaseContainer {
throw new ContainerException( "You cannot use concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
}
if ( ! array_key_exists( $class_name, $this->original_concretes ) ) {
$this->original_concretes[ $class_name ] = $this->extend( $class_name )->getConcrete( $concrete );
}
return $this->extend( $class_name )->setConcrete( $concrete );
}
/**
* Reset a replaced registration back to its original concrete.
*
* @param string $class_name The class name whose definition had been replaced.
* @return bool True if the registration has been reset, false if no replacement had been made for the specified class name.
*/
public function reset_replacement( string $class_name ) : bool {
if ( ! array_key_exists( $class_name, $this->original_concretes ) ) {
return false;
}
$this->extend( $class_name )->setConcrete( $this->original_concretes[ $class_name ] );
unset( $this->original_concretes[ $class_name ] );
return true;
}
/**
* Reset all the replaced registrations back to their original concretes.
*/
public function reset_all_replacements() {
foreach ( $this->original_concretes as $class_name => $concrete ) {
$this->extend( $class_name )->setConcrete( $concrete );
}
$this->original_concretes = array();
}
/**
* Reset all the cached resolutions, so any further "get" for shared definitions will generate the instance again.
*/
public function reset_all_resolved() {
foreach ( $this->definitions->getIterator() as $definition ) {
// setConcrete causes the cached resolved value to be forgotten.
$concrete = $definition->getConcrete();
$definition->setConcrete( $concrete );
$definition->forgetResolved();
}
}

View File

@ -217,6 +217,15 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
wc_get_container()->reset_all_resolved();
}
/**
* Reset all the class registration replacements in the dependency injection container,
* so any further "get" will return an instance of the class originally registered.
* For this to work with shared definitions 'reset_container_resolutions' is required too.
*/
public function reset_container_replacements() {
wc_get_container()->reset_all_replacements();
}
/**
* Reset the mock legacy proxy class so that all the registered mocks are unregistered.
*/

View File

@ -36,6 +36,10 @@ class ClassWithDependencies {
*/
public $dependency_class = null;
public function __construct() {
self::$instances_count++;
}
/**
* Initialize the class instance.
*
@ -45,8 +49,7 @@ class ClassWithDependencies {
* @param int $some_number Some number we need for some reason.
*/
final public function init( DependencyClass $dependency_class, int $some_number = self::SOME_NUMBER ) {
self::$instances_count++;
$this->dependency_class = $dependency_class;
$this->some_number = self::SOME_NUMBER;
$this->some_number = $some_number;
}
}

View File

@ -0,0 +1,12 @@
<?php
/**
* DerivedDependencyClass class file.
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class other classes depend on and is also a derived class.
*/
class DerivedDependencyClass extends DependencyClass {
}

View File

@ -9,6 +9,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ContainerException;
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithDependencies;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DerivedDependencyClass;
/**
* Tests for ExtendedContainer.
@ -30,7 +31,7 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
}
/**
* @testdox 'add' should throw an exception when trying to register a class not in the WooCommerce root namespace.
* @testDox 'add' should throw an exception when trying to register a class not in the WooCommerce root namespace.
*/
public function test_add_throws_when_trying_to_register_class_in_forbidden_namespace() {
$external_class = \WooCommerce::class;
@ -42,7 +43,7 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
}
/**
* @testdox 'add' should throw an exception when trying to register a concrete class not in the WooCommerce root namespace.
* @testDox 'add' should throw an exception when trying to register a concrete class not in the WooCommerce root namespace.
*/
public function test_add_throws_when_trying_to_register_concrete_class_in_forbidden_namespace() {
$external_class = \WooCommerce::class;
@ -54,7 +55,7 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
}
/**
* @testdox 'add' should allow registering classes in the WooCommerce root namespace.
* @testDox 'add' should allow registering classes in the WooCommerce root namespace.
*/
public function test_add_allows_registering_classes_in_woocommerce_root_namespace() {
$instance = new DependencyClass();
@ -65,7 +66,7 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
}
/**
* @testdox 'replace' should throw an exception when trying to replace a class that has not been previously registered.
* @testDox 'replace' should throw an exception when trying to replace a class that has not been previously registered.
*/
public function test_replace_throws_if_class_has_not_been_registered() {
$this->expectException( ContainerException::class );
@ -75,7 +76,7 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
}
/**
* @testdox 'replace'
* @testDox 'replace' should throw an exception when trying to use a class outside the Automattic\WooCommerce\ namespace as the replacement.
*/
public function test_replace_throws_if_concrete_not_in_woocommerce_root_namespace() {
$instance = new DependencyClass();
@ -90,7 +91,7 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
}
/**
* @testdox 'replace' should allow to replace existing registrations.
* @testDox 'replace' should allow to replace existing registrations with object instances.
*/
public function test_replace_allows_replacing_existing_registrations() {
$instance_1 = new DependencyClass();
@ -104,7 +105,7 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
}
/**
* @testdox 'replace' should allow to replace existing registrations with anonymous classes.
* @testDox 'replace' should allow to replace existing registrations with anonymous classes.
*/
public function test_replace_allows_replacing_existing_registrations_with_anonymous_classes() {
$instance_1 = new DependencyClass();
@ -113,12 +114,42 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
$this->sut->add( DependencyClass::class, $instance_1, true );
$this->assertSame( $instance_1, $this->sut->get( DependencyClass::class ) );
$this->sut->replace( DependencyClass::class, $instance_2, true );
$this->sut->replace( DependencyClass::class, $instance_2 );
$this->assertSame( $instance_2, $this->sut->get( DependencyClass::class ) );
}
/**
* @testdox 'reset_all_resolved' should discard cached resolutions for classes registered as 'shared'.
* @testDox 'replace' should allow replacing existing registrations with other class names.
*/
public function test_replace_allows_replacing_existing_registrations_with_class_names() {
$this->sut->add( DependencyClass::class, new DependencyClass(), true );
$this->assertInstanceOf( DependencyClass::class, $this->sut->get( DependencyClass::class ) );
$this->sut->replace( DependencyClass::class, DerivedDependencyClass::class );
$this->assertInstanceOf( DerivedDependencyClass::class, $this->sut->get( DependencyClass::class ) );
}
/**
* @testDox 'init' should_be executed when resolving the class in the instance passed to 'replace'
*/
public function test_init_is_executed_when_resolving_the_class_in_the_instance_passed_to_replace() {
$this->sut->add( DependencyClass::class );
$this->sut->add( ClassWithDependencies::class )->addArgument( DependencyClass::class );
$this->sut->get( ClassWithDependencies::class );
$this->assertInstanceOf( DependencyClass::class, $this->sut->get( ClassWithDependencies::class )->dependency_class );
$derived_class = new class() extends ClassWithDependencies {};
$this->sut->replace( ClassWithDependencies::class, $derived_class );
$this->sut->replace( DependencyClass::class, DerivedDependencyClass::class );
$replaced_instance = $this->sut->get( ClassWithDependencies::class );
$this->assertEquals( $derived_class, $replaced_instance );
$this->assertInstanceOf( DerivedDependencyClass::class, $replaced_instance->dependency_class );
}
/**
* @testDox 'reset_all_resolved' should discard cached resolutions for classes registered as 'shared'.
*/
public function test_reset_all_resolved_discards_cached_shared_resolutions() {
$this->sut->add( DependencyClass::class );
@ -137,4 +168,53 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
$this->sut->get( ClassWithDependencies::class );
$this->assertEquals( 2, ClassWithDependencies::$instances_count );
}
/**
* @testDox 'reset_replacement' should revert a replaced definition back to its original concrete.
*/
public function test_reset_replacement_returns_a_replaced_definition_back_to_its_original_concrete() {
$this->sut->add( DependencyClass::class, new DependencyClass(), false );
$this->assertInstanceOf( DependencyClass::class, $this->sut->get( DependencyClass::class ) );
$this->sut->replace( DependencyClass::class, DerivedDependencyClass::class );
$this->assertInstanceOf( DerivedDependencyClass::class, $this->sut->get( DependencyClass::class ) );
$rederived_instance = new class() extends DerivedDependencyClass {};
$this->sut->replace( DependencyClass::class, $rederived_instance );
$this->assertSame( $rederived_instance, $this->sut->get( DependencyClass::class ) );
$was_reset = $this->sut->reset_replacement( DependencyClass::class );
$this->assertTrue( $was_reset );
$this->assertInstanceOf( DependencyClass::class, $this->sut->get( DependencyClass::class ) );
}
/**
* @testDox 'reset_replacement' returns false if the given class hadn't got a replacement.
*/
public function test_reset_replacement_returns_false_if_the_given_class_hadnt_got_a_replacement() {
$this->assertFalse( $this->sut->reset_replacement( DependencyClass::class ) );
}
/**
* @testDox 'reset_all_replacements' should revert all the replaced definitions back to their original concretes.
*/
public function test_reset_all_replacements_reverts_all_the_replaced_definitions_back_to_their_original_concretes() {
$this->sut->add( DependencyClass::class );
$this->sut->add( ClassWithDependencies::class )->addArgument( DependencyClass::class );
$this->assertInstanceOf( DependencyClass::class, $this->sut->get( ClassWithDependencies::class )->dependency_class );
$this->assertInstanceOf( ClassWithDependencies::class, $this->sut->get( ClassWithDependencies::class ) );
$this->sut->replace( DependencyClass::class, DerivedDependencyClass::class );
$derived_class = new class() extends ClassWithDependencies {};
$this->sut->replace( ClassWithDependencies::class, $derived_class );
$this->assertInstanceOf( DerivedDependencyClass::class, $this->sut->get( ClassWithDependencies::class )->dependency_class );
$this->assertSame( $derived_class, $this->sut->get( ClassWithDependencies::class ) );
$this->sut->reset_all_replacements();
$this->assertInstanceOf( DependencyClass::class, $this->sut->get( ClassWithDependencies::class )->dependency_class );
$this->assertInstanceOf( ClassWithDependencies::class, $this->sut->get( ClassWithDependencies::class ) );
}
}