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
*
@ -227,9 +234,38 @@ class Injector {
* Sets the default global injector instance.
*
* @param Injector $instance
* @return Injector Reference to new active 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
* 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) {
self::$instance = $instance;
global $_SINGLETONS;
$_SINGLETONS['Config'] = $instance;
return $instance;
}
/**
@ -215,23 +217,27 @@ class Config {
* {@link Config} instance.
*
* 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.
*
* @return Config Reference to new active Config instance
*/
public static function nest() {
$current = self::$instance;
$new = clone $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
* Config object was copied from.
*
* @return Config Reference to new active Config instance
*/
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.
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.
* 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
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?

View File

@ -14,6 +14,9 @@ class DirectorTest extends SapphireTest {
public function setUp() {
parent::setUp();
// Required for testRequestFilterInDirectorTest
Injector::nest();
// Hold the original request URI once so it doesn't get overwritten
if(!self::$originalRequestURI) {
self::$originalRequestURI = $_SERVER['REQUEST_URI'];
@ -42,7 +45,7 @@ class DirectorTest extends SapphireTest {
// TODO Remove director rule, currently API doesnt allow this
// 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
$_SERVER['REQUEST_URI'] = self::$originalRequestURI;
@ -53,6 +56,8 @@ class DirectorTest extends SapphireTest {
}
}
Injector::unnest();
parent::tearDown();
}
@ -80,7 +85,7 @@ class DirectorTest extends SapphireTest {
$rootURL = Director::protocolAndHost();
$_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/";
Director::setBaseURL('/mysite/');
Config::inst()->update('Director', 'alternate_base_url', '/mysite/');
// Test already absolute url
$this->assertEquals($rootURL, Director::absoluteURL($rootURL));
@ -387,8 +392,6 @@ class DirectorTest extends SapphireTest {
$processor = new RequestProcessor(array($filter));
$currentProcessor = Injector::inst()->get('RequestProcessor');
Injector::inst()->registerService($processor, 'RequestProcessor');
$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
$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 {
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() {
$this->assertEquals(Config::inst()->get('ConfigStaticTest_First', 'first', Config::FIRST_SET),
array('test_1'));

View File

@ -12,6 +12,24 @@ define('TEST_SERVICES', dirname(__FILE__) . '/testservices');
*/
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() {
$injector = Injector::inst();
$this->assertTrue($injector->getConfigLocator() instanceof SilverStripeServiceConfigurationLocator,
@ -563,6 +581,64 @@ class InjectorTest extends SapphireTest {
$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 {