refactor(testing): move common testing logic into test_injector

Before, all test framework wrappers (internal for dart and js/ts,
angular2_test for dart and testing for js/ts) had similar logic to
keep track of current global test injector and test provider list.
This change wraps that logic into one class managed by the test
injector.

Closes #5920
This commit is contained in:
Julie Ralph 2015-12-08 19:03:21 -08:00
parent 630d93150a
commit e748adda2e
13 changed files with 120 additions and 94 deletions

View File

@ -23,7 +23,7 @@ import {
defaultKeyValueDiffers, defaultKeyValueDiffers,
ChangeDetectorGenConfig ChangeDetectorGenConfig
} from 'angular2/src/core/change_detection/change_detection'; } from 'angular2/src/core/change_detection/change_detection';
import {ExceptionHandler} from 'angular2/src/facade/exceptions'; import {BaseException, ExceptionHandler} from 'angular2/src/facade/exceptions';
import {PipeResolver} from 'angular2/src/core/linker/pipe_resolver'; import {PipeResolver} from 'angular2/src/core/linker/pipe_resolver';
import {XHR} from 'angular2/src/compiler/xhr'; import {XHR} from 'angular2/src/compiler/xhr';
@ -131,11 +131,62 @@ function _runtimeCompilerBindings() {
]; ];
} }
export class TestInjector {
private _instantiated: boolean = false;
private _injector: Injector = null;
private _providers: Array<Type | Provider | any[]> = [];
reset() {
this._injector = null;
this._providers = [];
this._instantiated = false;
}
addProviders(providers: Array<Type | Provider | any[]>) {
if (this._instantiated) {
throw new BaseException('Cannot add providers after test injector is instantiated');
}
this._providers = ListWrapper.concat(this._providers, providers);
}
createInjector() {
var rootInjector = Injector.resolveAndCreate(_getRootProviders());
this._injector = rootInjector.resolveAndCreateChild(ListWrapper.concat(
ListWrapper.concat(_getAppBindings(), _runtimeCompilerBindings()), this._providers));
this._instantiated = true;
return this._injector;
}
execute(fn: FunctionWithParamTokens): any {
if (!this._instantiated) {
this.createInjector();
}
return fn.execute(this._injector);
}
}
var _testInjector: TestInjector = null;
export function getTestInjector() {
if (_testInjector == null) {
_testInjector = new TestInjector();
}
return _testInjector;
}
/**
* @deprecated Use TestInjector#createInjector() instead.
*/
export function createTestInjector(providers: Array<Type | Provider | any[]>): Injector { export function createTestInjector(providers: Array<Type | Provider | any[]>): Injector {
var rootInjector = Injector.resolveAndCreate(_getRootProviders()); var rootInjector = Injector.resolveAndCreate(_getRootProviders());
return rootInjector.resolveAndCreateChild(ListWrapper.concat(_getAppBindings(), providers)); return rootInjector.resolveAndCreateChild(ListWrapper.concat(_getAppBindings(), providers));
} }
/**
* @deprecated Use TestInjector#createInjector() instead.
*/
export function createTestInjectorWithRuntimeCompiler( export function createTestInjectorWithRuntimeCompiler(
providers: Array<Type | Provider | any[]>): Injector { providers: Array<Type | Provider | any[]>): Injector {
return createTestInjector(ListWrapper.concat(_runtimeCompilerBindings(), providers)); return createTestInjector(ListWrapper.concat(_runtimeCompilerBindings(), providers));
@ -159,7 +210,8 @@ export function createTestInjectorWithRuntimeCompiler(
* ``` * ```
* *
* Notes: * Notes:
* - inject is currently a function because of some Traceur limitation the syntax should eventually * - inject is currently a function because of some Traceur limitation the syntax should
* eventually
* becomes `it('...', @Inject (object: AClass, async: AsyncTestCompleter) => { ... });` * becomes `it('...', @Inject (object: AClass, async: AsyncTestCompleter) => { ... });`
* *
* @param {Array} tokens * @param {Array} tokens

View File

@ -7,10 +7,11 @@ import {ListWrapper} from 'angular2/src/facade/collection';
import {bind} from 'angular2/core'; import {bind} from 'angular2/core';
import { import {
createTestInjectorWithRuntimeCompiler,
FunctionWithParamTokens, FunctionWithParamTokens,
inject, inject,
injectAsync injectAsync,
TestInjector,
getTestInjector
} from './test_injector'; } from './test_injector';
export {inject, injectAsync} from './test_injector'; export {inject, injectAsync} from './test_injector';
@ -92,14 +93,10 @@ var jsmIt = _global.it;
var jsmIIt = _global.fit; var jsmIIt = _global.fit;
var jsmXIt = _global.xit; var jsmXIt = _global.xit;
var testProviders; var testInjector: TestInjector = getTestInjector();
var injector;
// Reset the test providers before each test. // Reset the test providers before each test.
jsmBeforeEach(() => { jsmBeforeEach(() => { testInjector.reset(); });
testProviders = [];
injector = null;
});
/** /**
* Allows overriding default providers of the test injector, * Allows overriding default providers of the test injector,
@ -115,8 +112,9 @@ export function beforeEachProviders(fn): void {
jsmBeforeEach(() => { jsmBeforeEach(() => {
var providers = fn(); var providers = fn();
if (!providers) return; if (!providers) return;
testProviders = [...testProviders, ...providers]; try {
if (injector !== null) { testInjector.addProviders(providers);
} catch (e) {
throw new Error('beforeEachProviders was called after the injector had ' + throw new Error('beforeEachProviders was called after the injector had ' +
'been used in a beforeEach or it block. This invalidates the ' + 'been used in a beforeEach or it block. This invalidates the ' +
'test injector'); 'test injector');
@ -188,17 +186,13 @@ function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | An
if (testFn instanceof FunctionWithParamTokens) { if (testFn instanceof FunctionWithParamTokens) {
jsmFn(name, (done) => { jsmFn(name, (done) => {
if (!injector) {
injector = createTestInjectorWithRuntimeCompiler(testProviders);
}
var finishCallback = () => { var finishCallback = () => {
// Wait one more event loop to make sure we catch unreturned promises and // Wait one more event loop to make sure we catch unreturned promises and
// promise rejections. // promise rejections.
setTimeout(done, 0); setTimeout(done, 0);
}; };
var returnedTestValue = var returnedTestValue =
runInTestZone(() => testFn.execute(injector), finishCallback, done.fail); runInTestZone(() => testInjector.execute(testFn), finishCallback, done.fail);
if (testFn.isAsync) { if (testFn.isAsync) {
if (_isPromiseLike(returnedTestValue)) { if (_isPromiseLike(returnedTestValue)) {
@ -243,11 +237,9 @@ export function beforeEach(fn: FunctionWithParamTokens | AnyTestFn): void {
// promise rejections. // promise rejections.
setTimeout(done, 0); setTimeout(done, 0);
}; };
if (!injector) {
injector = createTestInjectorWithRuntimeCompiler(testProviders);
}
var returnedTestValue = runInTestZone(() => fn.execute(injector), finishCallback, done.fail); var returnedTestValue =
runInTestZone(() => testInjector.execute(fn), finishCallback, done.fail);
if (fn.isAsync) { if (fn.isAsync) {
if (_isPromiseLike(returnedTestValue)) { if (_isPromiseLike(returnedTestValue)) {
(<Promise<any>>returnedTestValue).then(null, (err) => { done.fail(err); }); (<Promise<any>>returnedTestValue).then(null, (err) => { done.fail(err); });

View File

@ -21,20 +21,23 @@ import 'package:angular2/src/core/reflection/reflection.dart';
import 'package:angular2/src/core/reflection/reflection_capabilities.dart'; import 'package:angular2/src/core/reflection/reflection_capabilities.dart';
import 'package:angular2/src/core/di/provider.dart' show bind; import 'package:angular2/src/core/di/provider.dart' show bind;
import 'package:angular2/src/core/di/injector.dart' show Injector;
import 'package:angular2/src/facade/collection.dart' show StringMapWrapper; import 'package:angular2/src/facade/collection.dart' show StringMapWrapper;
import 'test_injector.dart'; import 'test_injector.dart';
export 'test_injector.dart' show inject; export 'test_injector.dart' show inject;
List _testBindings = []; TestInjector _testInjector = getTestInjector();
Injector _injector;
bool _isCurrentTestAsync; bool _isCurrentTestAsync;
Future _currentTestFuture;
bool _inIt = false; bool _inIt = false;
class AsyncTestCompleter { class AsyncTestCompleter {
final _completer = new Completer(); final _completer = new Completer();
AsyncTestCompleter() {
_currentTestFuture = this.future;
}
void done() { void done() {
_completer.complete(); _completer.complete();
} }
@ -50,10 +53,11 @@ void testSetup() {
// - Priority 1: create the test injector to be used in beforeEach() and it() // - Priority 1: create the test injector to be used in beforeEach() and it()
gns.beforeEach(() { gns.beforeEach(() {
_testBindings.clear(); _testInjector.reset();
_currentTestFuture = null;
}, priority: 3); }, priority: 3);
var completerBinding = bind(AsyncTestCompleter).toFactory(() { var completerProvider = bind(AsyncTestCompleter).toFactory(() {
// Mark the test as async when an AsyncTestCompleter is injected in an it(), // Mark the test as async when an AsyncTestCompleter is injected in an it(),
if (!_inIt) throw 'AsyncTestCompleter can only be injected in an "it()"'; if (!_inIt) throw 'AsyncTestCompleter can only be injected in an "it()"';
_isCurrentTestAsync = true; _isCurrentTestAsync = true;
@ -62,15 +66,14 @@ void testSetup() {
gns.beforeEach(() { gns.beforeEach(() {
_isCurrentTestAsync = false; _isCurrentTestAsync = false;
_testBindings.add(completerBinding); _testInjector.addProviders([completerProvider]);
_injector = createTestInjectorWithRuntimeCompiler(_testBindings);
}, priority: 1); }, priority: 1);
} }
/** /**
* Allows overriding default bindings defined in test_injector.js. * Allows overriding default providers defined in test_injector.js.
* *
* The given function must return a list of DI bindings. * The given function must return a list of DI providers.
* *
* Example: * Example:
* *
@ -81,8 +84,8 @@ void testSetup() {
*/ */
void beforeEachProviders(Function fn) { void beforeEachProviders(Function fn) {
gns.beforeEach(() { gns.beforeEach(() {
var bindings = fn(); var providers = fn();
if (bindings != null) _testBindings.addAll(bindings); if (providers != null) _testInjector.addProviders(providers);
}, priority: 2); }, priority: 2);
} }
@ -95,7 +98,7 @@ void beforeEach(fn) {
if (fn is! FunctionWithParamTokens) fn = if (fn is! FunctionWithParamTokens) fn =
new FunctionWithParamTokens([], fn, false); new FunctionWithParamTokens([], fn, false);
gns.beforeEach(() { gns.beforeEach(() {
fn.execute(_injector); _testInjector.execute(fn);
}); });
} }
@ -104,9 +107,9 @@ void _it(gnsFn, name, fn) {
new FunctionWithParamTokens([], fn, false); new FunctionWithParamTokens([], fn, false);
gnsFn(name, () { gnsFn(name, () {
_inIt = true; _inIt = true;
fn.execute(_injector); _testInjector.execute(fn);
_inIt = false; _inIt = false;
if (_isCurrentTestAsync) return _injector.get(AsyncTestCompleter).future; if (_isCurrentTestAsync) return _currentTestFuture;
}); });
} }

View File

@ -5,11 +5,7 @@ import {NgZoneZone} from 'angular2/src/core/zone/ng_zone';
import {provide} from 'angular2/core'; import {provide} from 'angular2/core';
import { import {TestInjector, getTestInjector, FunctionWithParamTokens, inject} from './test_injector';
createTestInjectorWithRuntimeCompiler,
FunctionWithParamTokens,
inject
} from './test_injector';
import {browserDetection} from './utils'; import {browserDetection} from './utils';
export {inject} from './test_injector'; export {inject} from './test_injector';
@ -48,7 +44,7 @@ var inIt = false;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500; jasmine.DEFAULT_TIMEOUT_INTERVAL = 500;
var globalTimeOut = browserDetection.isSlow ? 3000 : jasmine.DEFAULT_TIMEOUT_INTERVAL; var globalTimeOut = browserDetection.isSlow ? 3000 : jasmine.DEFAULT_TIMEOUT_INTERVAL;
var testProviders; var testInjector = getTestInjector();
/** /**
* Mechanism to run `beforeEach()` functions of Angular tests. * Mechanism to run `beforeEach()` functions of Angular tests.
@ -62,16 +58,17 @@ class BeforeEachRunner {
beforeEach(fn: FunctionWithParamTokens | SyncTestFn): void { this._fns.push(fn); } beforeEach(fn: FunctionWithParamTokens | SyncTestFn): void { this._fns.push(fn); }
run(injector): void { run(): void {
if (this._parent) this._parent.run(injector); if (this._parent) this._parent.run();
this._fns.forEach((fn) => { this._fns.forEach((fn) => {
return isFunction(fn) ? (<SyncTestFn>fn)() : (<FunctionWithParamTokens>fn).execute(injector); return isFunction(fn) ? (<SyncTestFn>fn)() :
(testInjector.execute(<FunctionWithParamTokens>fn));
}); });
} }
} }
// Reset the test providers before each test // Reset the test providers before each test
jsmBeforeEach(() => { testProviders = []; }); jsmBeforeEach(() => { testInjector.reset(); });
function _describe(jsmFn, ...args) { function _describe(jsmFn, ...args) {
var parentRunner = runnerStack.length === 0 ? null : runnerStack[runnerStack.length - 1]; var parentRunner = runnerStack.length === 0 ? null : runnerStack[runnerStack.length - 1];
@ -120,7 +117,7 @@ export function beforeEachProviders(fn): void {
jsmBeforeEach(() => { jsmBeforeEach(() => {
var providers = fn(); var providers = fn();
if (!providers) return; if (!providers) return;
testProviders = [...testProviders, ...providers]; testInjector.addProviders(providers);
}); });
} }
@ -150,18 +147,17 @@ function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | An
} }
}); });
var injector = createTestInjectorWithRuntimeCompiler([...testProviders, completerProvider]); testInjector.addProviders([completerProvider]);
runner.run(injector); runner.run();
inIt = true; inIt = true;
testFn.execute(injector); testInjector.execute(testFn);
inIt = false; inIt = false;
}, timeOut); }, timeOut);
} else { } else {
jsmFn(name, () => { jsmFn(name, () => {
var injector = createTestInjectorWithRuntimeCompiler(testProviders); runner.run();
runner.run(injector); testInjector.execute(testFn);
testFn.execute(injector);
}, timeOut); }, timeOut);
} }
@ -170,14 +166,12 @@ function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | An
if ((<any>testFn).length === 0) { if ((<any>testFn).length === 0) {
jsmFn(name, () => { jsmFn(name, () => {
var injector = createTestInjectorWithRuntimeCompiler(testProviders); runner.run();
runner.run(injector);
(<SyncTestFn>testFn)(); (<SyncTestFn>testFn)();
}, timeOut); }, timeOut);
} else { } else {
jsmFn(name, (done) => { jsmFn(name, (done) => {
var injector = createTestInjectorWithRuntimeCompiler(testProviders); runner.run();
runner.run(injector);
(<AsyncTestFn>testFn)(done); (<AsyncTestFn>testFn)(done);
}, timeOut); }, timeOut);
} }

View File

@ -11,7 +11,6 @@ import "package:angular2/testing_internal.dart"
iit, iit,
expect, expect,
beforeEach, beforeEach,
createTestInjector,
beforeEachProviders, beforeEachProviders,
SpyObject, SpyObject,
proxy; proxy;

View File

@ -10,7 +10,6 @@ import "package:angular2/testing_internal.dart"
it, it,
expect, expect,
beforeEach, beforeEach,
createTestInjector,
beforeEachProviders, beforeEachProviders,
SpyObject, SpyObject,
proxy; proxy;

View File

@ -8,7 +8,6 @@ import "package:angular2/testing_internal.dart"
it, it,
expect, expect,
beforeEach, beforeEach,
createTestInjector,
beforeEachProviders, beforeEachProviders,
SpyObject, SpyObject,
proxy; proxy;

View File

@ -5,7 +5,6 @@ import {
it, it,
expect, expect,
beforeEach, beforeEach,
createTestInjectorWithRuntimeCompiler,
beforeEachProviders, beforeEachProviders,
SpyObject, SpyObject,
proxy proxy

View File

@ -5,7 +5,6 @@ import {
it, it,
expect, expect,
beforeEach, beforeEach,
createTestInjectorWithRuntimeCompiler,
beforeEachProviders, beforeEachProviders,
SpyObject, SpyObject,
proxy proxy

View File

@ -5,7 +5,6 @@ import {
it, it,
expect, expect,
beforeEach, beforeEach,
createTestInjectorWithRuntimeCompiler,
beforeEachProviders, beforeEachProviders,
SpyObject, SpyObject,
proxy proxy

View File

@ -7,8 +7,8 @@ import {
iit, iit,
expect, expect,
beforeEach, beforeEach,
createTestInjectorWithRuntimeCompiler,
beforeEachProviders, beforeEachProviders,
TestInjector,
TestComponentBuilder TestComponentBuilder
} from "angular2/testing_internal"; } from "angular2/testing_internal";
import {DOM} from 'angular2/src/platform/dom/dom_adapter'; import {DOM} from 'angular2/src/platform/dom/dom_adapter';
@ -102,12 +102,14 @@ export function main() {
beforeEachProviders(() => { beforeEachProviders(() => {
var uiRenderProtoViewStore = new RenderProtoViewRefStore(false); var uiRenderProtoViewStore = new RenderProtoViewRefStore(false);
uiRenderViewStore = new RenderViewWithFragmentsStore(false); uiRenderViewStore = new RenderViewWithFragmentsStore(false);
uiInjector = createTestInjectorWithRuntimeCompiler([ var testInjector = new TestInjector();
testInjector.addProviders([
provide(RenderProtoViewRefStore, {useValue: uiRenderProtoViewStore}), provide(RenderProtoViewRefStore, {useValue: uiRenderProtoViewStore}),
provide(RenderViewWithFragmentsStore, {useValue: uiRenderViewStore}), provide(RenderViewWithFragmentsStore, {useValue: uiRenderViewStore}),
provide(DomRenderer, {useClass: DomRenderer_}), provide(DomRenderer, {useClass: DomRenderer_}),
provide(Renderer, {useExisting: DomRenderer}) provide(Renderer, {useExisting: DomRenderer})
]); ]);
uiInjector = testInjector.createInjector();
var uiSerializer = uiInjector.get(Serializer); var uiSerializer = uiInjector.get(Serializer);
var domRenderer = uiInjector.get(DomRenderer); var domRenderer = uiInjector.get(DomRenderer);
var workerRenderProtoViewStore = new RenderProtoViewRefStore(true); var workerRenderProtoViewStore = new RenderProtoViewRefStore(true);

View File

@ -5,7 +5,6 @@ import {
it, it,
expect, expect,
beforeEach, beforeEach,
createTestInjectorWithRuntimeCompiler,
beforeEachProviders beforeEachProviders
} from 'angular2/testing_internal'; } from 'angular2/testing_internal';
import {SpyMessageBroker} from './spies'; import {SpyMessageBroker} from './spies';

View File

@ -1,11 +1,8 @@
library angular2_testing.angular2_testing; library angular2_testing.angular2_testing;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:test/src/backend/invoker.dart';
import 'package:test/src/backend/live_test.dart';
import 'package:angular2/angular2.dart'; import 'package:angular2/angular2.dart';
import 'package:angular2/src/core/di/injector.dart' show Injector;
import 'package:angular2/src/core/di/metadata.dart' show InjectMetadata; import 'package:angular2/src/core/di/metadata.dart' show InjectMetadata;
import 'package:angular2/src/core/di/exceptions.dart' show NoAnnotationError; import 'package:angular2/src/core/di/exceptions.dart' show NoAnnotationError;
import 'package:angular2/platform/browser_static.dart' show BrowserDomAdapter; import 'package:angular2/platform/browser_static.dart' show BrowserDomAdapter;
@ -31,6 +28,13 @@ void initAngularTests() {
reflector.reflectionCapabilities = new ReflectionCapabilities(); reflector.reflectionCapabilities = new ReflectionCapabilities();
} }
void _addTestInjectorTearDown() {
// Multiple resets are harmless.
tearDown(() {
_testInjector.reset();
});
}
/// Allows overriding default bindings defined in test_injector.dart. /// Allows overriding default bindings defined in test_injector.dart.
/// ///
/// The given function must return a list of DI providers. /// The given function must return a list of DI providers.
@ -45,13 +49,17 @@ void initAngularTests() {
/// ``` /// ```
void setUpProviders(Iterable<Provider> providerFactory()) { void setUpProviders(Iterable<Provider> providerFactory()) {
setUp(() { setUp(() {
if (_currentInjector != null) { try {
_testInjector.addProviders(providerFactory());
} catch(e) {
throw 'setUpProviders was called after the injector had ' throw 'setUpProviders was called after the injector had '
'been used in a setUp or test block. This invalidates the ' 'been used in a setUp or test block. This invalidates the '
'test injector'; 'test injector';
} }
_currentTestProviders.addAll(providerFactory());
}); });
_addTestInjectorTearDown();
} }
dynamic _runInjectableFunction(Function fn) { dynamic _runInjectableFunction(Function fn) {
@ -72,11 +80,8 @@ dynamic _runInjectableFunction(Function fn) {
tokens.add(token); tokens.add(token);
} }
if (_currentInjector == null) {
_currentInjector = createTestInjectorWithRuntimeCompiler(_currentTestProviders);
}
var injectFn = new FunctionWithParamTokens(tokens, fn, false); var injectFn = new FunctionWithParamTokens(tokens, fn, false);
return injectFn.execute(_currentInjector); return _testInjector.execute(injectFn);
} }
/// Use the test injector to get bindings and run a function. /// Use the test injector to get bindings and run a function.
@ -92,6 +97,8 @@ void ngSetUp(Function fn) {
setUp(() async { setUp(() async {
await _runInjectableFunction(fn); await _runInjectableFunction(fn);
}); });
_addTestInjectorTearDown();
} }
/// Add a test which can use the test injector. /// Add a test which can use the test injector.
@ -108,25 +115,8 @@ void ngTest(String description, Function fn,
test(description, () async { test(description, () async {
await _runInjectableFunction(fn); await _runInjectableFunction(fn);
}, testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform); }, testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform);
_addTestInjectorTearDown();
} }
final _providersExpando = final TestInjector _testInjector = getTestInjector();
new Expando<List<Provider>>('Providers for the current test');
final _injectorExpando =
new Expando<Injector>('Angular Injector for the current test');
List get _currentTestProviders {
if (_providersExpando[_currentTest] == null) {
return _providersExpando[_currentTest] = [];
}
return _providersExpando[_currentTest];
}
Injector get _currentInjector => _injectorExpando[_currentTest];
void set _currentInjector(Injector newInjector) {
_injectorExpando[_currentTest] = newInjector;
}
// TODO: warning, the Invoker.current.liveTest is not a settled API and is
// subject to change in future versions of package:test.
LiveTest get _currentTest => Invoker.current.liveTest;