Merge pull request #3058 from tractorcow/pulls/injector-stack-tests

API Injector supports nesting
This commit is contained in:
Hamish Friedlander 2014-05-06 11:35:32 +12:00
commit bbd7bba11f
7 changed files with 178 additions and 12 deletions

View File

@ -210,6 +210,13 @@ class Injector {
} }
} }
/**
* The injector instance this one was copied from when Injector::nest() was called.
*
* @var Injector
*/
protected $nestedFrom = null;
/** /**
* If a user wants to use the injector as a static reference * If a user wants to use the injector as a static reference
* *
@ -227,9 +234,38 @@ class Injector {
* Sets the default global injector instance. * Sets the default global injector instance.
* *
* @param Injector $instance * @param Injector $instance
* @return Injector Reference to new active Injector instance
*/ */
public static function set_inst(Injector $instance) { public static function set_inst(Injector $instance) {
self::$instance = $instance; return self::$instance = $instance;
}
/**
* Make the newly active {@link Injector} be a copy of the current active
* {@link Injector} instance.
*
* You can then make changes to the injector with methods such as
* {@link Injector::inst()->registerService()} which will be discarded
* upon a subsequent call to {@link Injector::unnest()}
*
* @return Injector Reference to new active Injector instance
*/
public static function nest() {
$current = self::$instance;
$new = clone $current;
$new->nestedFrom = $current;
return self::set_inst($new);
}
/**
* Change the active Injector back to the Injector instance the current active
* Injector object was copied from.
*
* @return Injector Reference to restored active Injector instance
*/
public static function unnest() {
return self::set_inst(self::$instance->nestedFrom);
} }
/** /**

View File

@ -201,13 +201,15 @@ class Config {
* A use case for replacing the active configuration set would be for * A use case for replacing the active configuration set would be for
* creating an isolated environment for unit tests. * creating an isolated environment for unit tests.
* *
* @return Config * @param Config $instance New instance of Config to assign
* @return Config Reference to new active Config instance
*/ */
public static function set_instance($instance) { public static function set_instance($instance) {
self::$instance = $instance; self::$instance = $instance;
global $_SINGLETONS; global $_SINGLETONS;
$_SINGLETONS['Config'] = $instance; $_SINGLETONS['Config'] = $instance;
return $instance;
} }
/** /**
@ -215,23 +217,27 @@ class Config {
* {@link Config} instance. * {@link Config} instance.
* *
* You can then make changes to the configuration by calling update and * You can then make changes to the configuration by calling update and
* remove on the new value returned by Config::inst(), and then discard * remove on the new value returned by {@link Config::inst()}, and then discard
* those changes later by calling unnest. * those changes later by calling unnest.
*
* @return Config Reference to new active Config instance
*/ */
public static function nest() { public static function nest() {
$current = self::$instance; $current = self::$instance;
$new = clone $current; $new = clone $current;
$new->nestedFrom = $current; $new->nestedFrom = $current;
self::set_instance($new); return self::set_instance($new);
} }
/** /**
* Change the active Config back to the Config instance the current active * Change the active Config back to the Config instance the current active
* Config object was copied from. * Config object was copied from.
*
* @return Config Reference to new active Config instance
*/ */
public static function unnest() { public static function unnest() {
self::set_instance(self::$instance->nestedFrom); return self::set_instance(self::$instance->nestedFrom);
} }
/** /**

View File

@ -7,3 +7,5 @@
user login name between sessions, and disable browser auto-completion on the username field. user login name between sessions, and disable browser auto-completion on the username field.
Note that users of certain browsers who have previously autofilled and saved login credentials Note that users of certain browsers who have previously autofilled and saved login credentials
will need to clear their password autofill history before this setting is properly respected. will need to clear their password autofill history before this setting is properly respected.
* Test cases that rely on updating and restoring `[api:Injector]` services may now take advantage
of the new `Injector::nest()` and `Injector::unnest()` methods to sandbox their alterations.

View File

@ -192,6 +192,30 @@ would
* Create a MySQLDatabase class, passing dbusername and dbpassword as the * Create a MySQLDatabase class, passing dbusername and dbpassword as the
parameters to the constructor parameters to the constructor
### Testing with Injector in a sandbox environment
In situations where injector states must be temporarily overridden, it is possible
to create nested Injector instances which may be later discarded, reverting the
application to the original state.
This is useful when writing test cases, as certain services may be necessary to
override for a single method call.
For instance, a temporary service can be registered and unregistered as below:
:::php
// Setup default service
Injector::inst()->registerService(new LiveService(), 'ServiceName');
// Test substitute service temporarily
Injector::nest();
Injector::inst()->registerService(new TestingService(), 'ServiceName');
$service = Injector::inst()->get('ServiceName');
// ... do something with $service
Injector::unnest();
// ... future requests for 'ServiceName' will return the LiveService instance
### What are Services? ### What are Services?

View File

@ -14,6 +14,9 @@ class DirectorTest extends SapphireTest {
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
// Required for testRequestFilterInDirectorTest
Injector::nest();
// Hold the original request URI once so it doesn't get overwritten // Hold the original request URI once so it doesn't get overwritten
if(!self::$originalRequestURI) { if(!self::$originalRequestURI) {
self::$originalRequestURI = $_SERVER['REQUEST_URI']; self::$originalRequestURI = $_SERVER['REQUEST_URI'];
@ -42,7 +45,7 @@ class DirectorTest extends SapphireTest {
// TODO Remove director rule, currently API doesnt allow this // TODO Remove director rule, currently API doesnt allow this
// Remove base URL override (setting to false reverts to default behaviour) // Remove base URL override (setting to false reverts to default behaviour)
Director::setBaseURL(false); Config::inst()->update('Director', 'alternate_base_url', false);
// Reinstate the original REQUEST_URI after it was modified by some tests // Reinstate the original REQUEST_URI after it was modified by some tests
$_SERVER['REQUEST_URI'] = self::$originalRequestURI; $_SERVER['REQUEST_URI'] = self::$originalRequestURI;
@ -53,6 +56,8 @@ class DirectorTest extends SapphireTest {
} }
} }
Injector::unnest();
parent::tearDown(); parent::tearDown();
} }
@ -80,7 +85,7 @@ class DirectorTest extends SapphireTest {
$rootURL = Director::protocolAndHost(); $rootURL = Director::protocolAndHost();
$_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/"; $_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/";
Director::setBaseURL('/mysite/'); Config::inst()->update('Director', 'alternate_base_url', '/mysite/');
// Test already absolute url // Test already absolute url
$this->assertEquals($rootURL, Director::absoluteURL($rootURL)); $this->assertEquals($rootURL, Director::absoluteURL($rootURL));
@ -387,8 +392,6 @@ class DirectorTest extends SapphireTest {
$processor = new RequestProcessor(array($filter)); $processor = new RequestProcessor(array($filter));
$currentProcessor = Injector::inst()->get('RequestProcessor');
Injector::inst()->registerService($processor, 'RequestProcessor'); Injector::inst()->registerService($processor, 'RequestProcessor');
$response = Director::test('some-dummy-url'); $response = Director::test('some-dummy-url');
@ -413,9 +416,6 @@ class DirectorTest extends SapphireTest {
// preCall 'false' will trigger an exception and prevent post call execution // preCall 'false' will trigger an exception and prevent post call execution
$this->assertEquals(2, $filter->postCalls); $this->assertEquals(2, $filter->postCalls);
// swap back otherwise our wrapping test execution request may fail in the post processing later
Injector::inst()->registerService($currentProcessor, 'RequestProcessor');
} }
} }

View File

@ -76,6 +76,28 @@ class ConfigStaticTest_Combined3 extends ConfigStaticTest_Combined2 {
class ConfigTest extends SapphireTest { class ConfigTest extends SapphireTest {
public function testNest() {
// Check basic config
$this->assertEquals(3, Config::inst()->get('ConfigTest_DefinesFooAndBar', 'foo'));
$this->assertEquals(3, Config::inst()->get('ConfigTest_DefinesFooAndBar', 'bar'));
// Test nest copies data
Config::nest();
$this->assertEquals(3, Config::inst()->get('ConfigTest_DefinesFooAndBar', 'foo'));
$this->assertEquals(3, Config::inst()->get('ConfigTest_DefinesFooAndBar', 'bar'));
// Test nested data can be updated
Config::inst()->update('ConfigTest_DefinesFooAndBar', 'foo', 4);
$this->assertEquals(4, Config::inst()->get('ConfigTest_DefinesFooAndBar', 'foo'));
$this->assertEquals(3, Config::inst()->get('ConfigTest_DefinesFooAndBar', 'bar'));
// Test unnest restores data
Config::unnest();
$this->assertEquals(3, Config::inst()->get('ConfigTest_DefinesFooAndBar', 'foo'));
$this->assertEquals(3, Config::inst()->get('ConfigTest_DefinesFooAndBar', 'bar'));
}
public function testUpdateStatic() { public function testUpdateStatic() {
$this->assertEquals(Config::inst()->get('ConfigStaticTest_First', 'first', Config::FIRST_SET), $this->assertEquals(Config::inst()->get('ConfigStaticTest_First', 'first', Config::FIRST_SET),
array('test_1')); array('test_1'));

View File

@ -12,6 +12,24 @@ define('TEST_SERVICES', dirname(__FILE__) . '/testservices');
*/ */
class InjectorTest extends SapphireTest { class InjectorTest extends SapphireTest {
protected $nestingLevel = 0;
public function setUp() {
parent::setUp();
$this->nestingLevel = 0;
}
public function tearDown() {
while($this->nestingLevel > 0) {
$this->nestingLevel--;
Config::unnest();
}
parent::tearDown();
}
public function testCorrectlyInitialised() { public function testCorrectlyInitialised() {
$injector = Injector::inst(); $injector = Injector::inst();
$this->assertTrue($injector->getConfigLocator() instanceof SilverStripeServiceConfigurationLocator, $this->assertTrue($injector->getConfigLocator() instanceof SilverStripeServiceConfigurationLocator,
@ -563,6 +581,64 @@ class InjectorTest extends SapphireTest {
$this->assertInstanceOf('TestObject', $injector->get('service')); $this->assertInstanceOf('TestObject', $injector->get('service'));
} }
/**
* Test nesting of injector
*/
public function testNest() {
// Outer nest to avoid interference with other
Injector::nest();
$this->nestingLevel++;
// Test services
$config = array(
'NewRequirementsBackend',
);
Injector::inst()->load($config);
$si = Injector::inst()->get('TestStaticInjections');
$this->assertInstanceOf('TestStaticInjections', $si);
$this->assertInstanceOf('NewRequirementsBackend', $si->backend);
$this->assertInstanceOf('MyParentClass', Injector::inst()->get('MyParentClass'));
$this->assertInstanceOf('MyChildClass', Injector::inst()->get('MyChildClass'));
// Test that nested injector values can be overridden
Injector::nest();
$this->nestingLevel++;
Injector::inst()->unregisterAllObjects();
$newsi = Injector::inst()->get('TestStaticInjections');
$newsi->backend = new OriginalRequirementsBackend();
Injector::inst()->registerService($newsi, 'TestStaticInjections');
Injector::inst()->registerService(new MyChildClass(), 'MyParentClass');
// Check that these overridden values are retrievable
$si = Injector::inst()->get('TestStaticInjections');
$this->assertInstanceOf('TestStaticInjections', $si);
$this->assertInstanceOf('OriginalRequirementsBackend', $si->backend);
$this->assertInstanceOf('MyParentClass', Injector::inst()->get('MyParentClass'));
$this->assertInstanceOf('MyParentClass', Injector::inst()->get('MyChildClass'));
// Test that unnesting restores expected behaviour
Injector::unnest();
$this->nestingLevel--;
$si = Injector::inst()->get('TestStaticInjections');
$this->assertInstanceOf('TestStaticInjections', $si);
$this->assertInstanceOf('NewRequirementsBackend', $si->backend);
$this->assertInstanceOf('MyParentClass', Injector::inst()->get('MyParentClass'));
$this->assertInstanceOf('MyChildClass', Injector::inst()->get('MyChildClass'));
// Test reset of cache
Injector::inst()->unregisterAllObjects();
$si = Injector::inst()->get('TestStaticInjections');
$this->assertInstanceOf('TestStaticInjections', $si);
$this->assertInstanceOf('NewRequirementsBackend', $si->backend);
$this->assertInstanceOf('MyParentClass', Injector::inst()->get('MyParentClass'));
$this->assertInstanceOf('MyChildClass', Injector::inst()->get('MyChildClass'));
// Return to nestingLevel 0
Injector::unnest();
$this->nestingLevel--;
}
} }
class InjectorTestConfigLocator extends SilverStripeServiceConfigurationLocator implements TestOnly { class InjectorTestConfigLocator extends SilverStripeServiceConfigurationLocator implements TestOnly {