chore(testing): Refactor test methods to have a uniform interface.

Remove FunctionWithParamTokens.

All test wrappers async, fakeAsync and inject now return just a Function instead of FunctionWithParamTokens. This makes them directly consumable by the test framework. Also the test framework code does not have to handle a union of Function and FunctionWithParamTokens everywhere.

The Function returned by the above methods are considered asynchronous by the test framework if they return a Promise, synchronous otherwise.

Closes #8257
This commit is contained in:
Vikram Subramanian 2016-04-26 13:06:50 -07:00 committed by vikerman
parent d2efac18ed
commit 35cd0ded22
11 changed files with 186 additions and 272 deletions

View File

@ -0,0 +1,4 @@
// This symbol is not used on the Dart side. This exists just as a stub.
async(Function fn) {
throw 'async() test wrapper not available for Dart.';
}

View File

@ -0,0 +1,25 @@
/**
* Wraps a test function in an asynchronous test zone. The test will automatically
* complete when all asynchronous calls within this zone are done. Can be used
* to wrap an {@link inject} call.
*
* Example:
*
* ```
* it('...', async(inject([AClass], (object) => {
* object.doSomething.then(() => {
* expect(...);
* })
* });
* ```
*/
export function async(fn: Function): Function {
return () => {
return new Promise<void>((finishCallback, failCallback) => {
var AsyncTestZoneSpec = Zone['AsyncTestZoneSpec'];
var testZoneSpec = new AsyncTestZoneSpec(finishCallback, failCallback, 'test');
var testZone = Zone.current.fork(testZoneSpec);
return testZone.run(fn);
});
}
}

View File

@ -0,0 +1,13 @@
import {PromiseCompleter} from 'angular2/src/facade/promise';
/**
* Injectable completer that allows signaling completion of an asynchronous test. Used internally.
*/
export class AsyncTestCompleter {
private _completer = new PromiseCompleter<any>();
done(value?: any) { this._completer.resolve(value); }
fail(error?: any, stackTrace?: string) { this._completer.reject(error, stackTrace); }
get promise(): Promise<any> { return this._completer.promise; }
}

View File

@ -4,8 +4,6 @@ import 'dart:async' show runZoned, ZoneSpecification;
import 'package:quiver/testing/async.dart' as quiver;
import 'package:angular2/src/facade/exceptions.dart' show BaseException;
import 'test_injector.dart' show getTestInjector, FunctionWithParamTokens;
const _u = const Object();
quiver.FakeAsync _fakeAsync = null;
@ -22,24 +20,11 @@ quiver.FakeAsync _fakeAsync = null;
*
* Returns a `Function` that wraps [fn].
*/
Function fakeAsync(dynamic /* Function | FunctionWithParamTokens */ fn) {
Function fakeAsync(Function fn) {
if (_fakeAsync != null) {
throw 'fakeAsync() calls can not be nested';
}
Function innerFn = null;
if (fn is FunctionWithParamTokens) {
if (fn.isAsync) {
throw 'Cannot wrap async test with fakeAsync';
}
innerFn = () { getTestInjector().execute(fn); };
} else if (fn is Function) {
innerFn = fn;
} else {
throw 'fakeAsync can wrap only test functions but got object of type ' +
fn.runtimeType.toString();
}
return ([a0 = _u,
a1 = _u,
a2 = _u,
@ -58,7 +43,7 @@ Function fakeAsync(dynamic /* Function | FunctionWithParamTokens */ fn) {
List args = [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9]
.takeWhile((a) => a != _u)
.toList();
var res = Function.apply(innerFn, args);
var res = Function.apply(fn, args);
_fakeAsync.flushMicrotasks();
if (async.periodicTimerCount > 0) {

View File

@ -1,5 +1,5 @@
import {BaseException} from 'angular2/src/facade/exceptions';
import {getTestInjector, FunctionWithParamTokens} from './test_injector';
import {getTestInjector} from './test_injector';
let _FakeAsyncTestZoneSpecType = Zone['FakeAsyncTestZoneSpec'];
@ -19,7 +19,7 @@ let _FakeAsyncTestZoneSpecType = Zone['FakeAsyncTestZoneSpec'];
* @param fn
* @returns {Function} The function wrapped to be executed in the fakeAsync zone
*/
export function fakeAsync(fn: Function | FunctionWithParamTokens): Function {
export function fakeAsync(fn: Function): Function {
if (Zone.current.get('FakeAsyncTestZoneSpec') != null) {
throw new BaseException('fakeAsync() calls can not be nested');
}
@ -27,20 +27,9 @@ export function fakeAsync(fn: Function | FunctionWithParamTokens): Function {
let fakeAsyncTestZoneSpec = new _FakeAsyncTestZoneSpecType();
let fakeAsyncZone = Zone.current.fork(fakeAsyncTestZoneSpec);
let innerTestFn: Function = null;
if (fn instanceof FunctionWithParamTokens) {
if (fn.isAsync) {
throw new BaseException('Cannot wrap async test with fakeAsync');
}
innerTestFn = () => { getTestInjector().execute(fn as FunctionWithParamTokens); };
} else {
innerTestFn = fn;
}
return function(...args) {
let res = fakeAsyncZone.run(() => {
let res = innerTestFn(...args);
let res = fn(...args);
flushMicrotasks();
return res;
});

View File

@ -3,6 +3,11 @@ import {BaseException, ExceptionHandler} from 'angular2/src/facade/exceptions';
import {ListWrapper} from 'angular2/src/facade/collection';
import {FunctionWrapper, isPresent, Type} from 'angular2/src/facade/lang';
import {async} from './async';
import {AsyncTestCompleter} from './async_test_completer';
export {async} from './async';
export class TestInjector {
private _instantiated: boolean = false;
@ -35,15 +40,19 @@ export class TestInjector {
return this._injector;
}
execute(fn: FunctionWithParamTokens): any {
var additionalProviders = fn.additionalProviders();
if (additionalProviders.length > 0) {
this.addProviders(additionalProviders);
}
get(token: any) {
if (!this._instantiated) {
this.createInjector();
}
return fn.execute(this._injector);
return this._injector.get(token);
}
execute(tokens: any[], fn: Function): any {
if (!this._instantiated) {
this.createInjector();
}
var params = tokens.map(t => this._injector.get(t));
return FunctionWrapper.apply(fn, params);
}
}
@ -117,22 +126,47 @@ export function resetBaseTestProviders() {
*
* @param {Array} tokens
* @param {Function} fn
* @return {FunctionWithParamTokens}
* @return {Function}
*/
export function inject(tokens: any[], fn: Function): FunctionWithParamTokens {
return new FunctionWithParamTokens(tokens, fn, false);
export function inject(tokens: any[], fn: Function): Function {
let testInjector = getTestInjector();
if (tokens.indexOf(AsyncTestCompleter) >= 0) {
// Return an async test method that returns a Promise if AsyncTestCompleter is one of the
// injected tokens.
return () => {
let completer: AsyncTestCompleter = testInjector.get(AsyncTestCompleter);
testInjector.execute(tokens, fn);
return completer.promise;
}
} else {
// Return a synchronous test method with the injected tokens.
return () => { return getTestInjector().execute(tokens, fn); };
}
}
export class InjectSetupWrapper {
constructor(private _providers: () => any) {}
inject(tokens: any[], fn: Function): FunctionWithParamTokens {
return new FunctionWithParamTokens(tokens, fn, false, this._providers);
private _addProviders() {
var additionalProviders = this._providers();
if (additionalProviders.length > 0) {
getTestInjector().addProviders(additionalProviders);
}
}
inject(tokens: any[], fn: Function): Function {
return () => {
this._addProviders();
return inject(tokens, fn)();
}
}
/** @Deprecated {use async(withProviders().inject())} */
injectAsync(tokens: any[], fn: Function): FunctionWithParamTokens {
return new FunctionWithParamTokens(tokens, fn, true, this._providers);
injectAsync(tokens: any[], fn: Function): Function {
return () => {
this._addProviders();
return injectAsync(tokens, fn)();
}
}
}
@ -158,53 +192,8 @@ export function withProviders(providers: () => any) {
*
* @param {Array} tokens
* @param {Function} fn
* @return {FunctionWithParamTokens}
* @return {Function}
*/
export function injectAsync(tokens: any[], fn: Function): FunctionWithParamTokens {
return new FunctionWithParamTokens(tokens, fn, true);
}
/**
* Wraps a test function in an asynchronous test zone. The test will automatically
* complete when all asynchronous calls within this zone are done. Can be used
* to wrap an {@link inject} call.
*
* Example:
*
* ```
* it('...', async(inject([AClass], (object) => {
* object.doSomething.then(() => {
* expect(...);
* })
* });
* ```
*/
export function async(fn: Function | FunctionWithParamTokens): FunctionWithParamTokens {
if (fn instanceof FunctionWithParamTokens) {
fn.isAsync = true;
return fn;
} else if (fn instanceof Function) {
return new FunctionWithParamTokens([], fn, true);
} else {
throw new BaseException('argument to async must be a function or inject(<Function>)');
}
}
function emptyArray(): Array<any> {
return [];
}
export class FunctionWithParamTokens {
constructor(private _tokens: any[], public fn: Function, public isAsync: boolean,
public additionalProviders: () => any = emptyArray) {}
/**
* Returns the value of the executed function.
*/
execute(injector: ReflectiveInjector): any {
var params = this._tokens.map(t => injector.get(t));
return FunctionWrapper.apply(this.fn, params);
}
hasToken(token: any): boolean { return this._tokens.indexOf(token) > -1; }
export function injectAsync(tokens: any[], fn: Function): Function {
return async(inject(tokens, fn));
}

View File

@ -2,18 +2,9 @@
* Public Test Library for unit testing Angular2 Applications. Uses the
* Jasmine framework.
*/
import {global} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {bind} from 'angular2/core';
import {global, isPromise} from 'angular2/src/facade/lang';
import {
FunctionWithParamTokens,
inject,
async,
injectAsync,
TestInjector,
getTestInjector
} from './test_injector';
import {inject, async, injectAsync, TestInjector, getTestInjector} from './test_injector';
export {inject, async, injectAsync} from './test_injector';
@ -73,22 +64,6 @@ export var fdescribe: Function = _global.fdescribe;
*/
export var xdescribe: Function = _global.xdescribe;
/**
* Signature for a synchronous test function (no arguments).
*/
export type SyncTestFn = () => void;
/**
* Signature for an asynchronous test function which takes a
* `done` callback.
*/
export type AsyncTestFn = (done: () => void) => void;
/**
* Signature for any simple testing function.
*/
export type AnyTestFn = SyncTestFn | AsyncTestFn | Function;
var jsmBeforeEach = _global.beforeEach;
var jsmIt = _global.it;
var jsmIIt = _global.fit;
@ -123,35 +98,27 @@ export function beforeEachProviders(fn): void {
});
}
function runInAsyncTestZone(fnToExecute, finishCallback: Function, failCallback: Function,
testName = ''): any {
var AsyncTestZoneSpec = Zone['AsyncTestZoneSpec'];
var testZoneSpec = new AsyncTestZoneSpec(finishCallback, failCallback, testName);
var testZone = Zone.current.fork(testZoneSpec);
return testZone.run(fnToExecute);
}
function _isPromiseLike(input): boolean {
return input && !!(input.then);
}
function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | AnyTestFn,
testTimeOut: number): void {
var timeOut = testTimeOut;
if (testFn instanceof FunctionWithParamTokens) {
let testFnT = testFn;
jsmFn(name, (done) => {
if (testFnT.isAsync) {
runInAsyncTestZone(() => testInjector.execute(testFnT), done, done.fail, name);
function _wrapTestFn(fn: Function) {
// Wraps a test or beforeEach function to handle synchronous and asynchronous execution.
return (done: any) => {
if (fn.length === 0) {
let retVal = fn();
if (isPromise(retVal)) {
// Asynchronous test function - wait for completion.
(<Promise<any>>retVal).then(done, done.fail);
} else {
testInjector.execute(testFnT);
// Synchronous test function - complete immediately.
done();
}
}, timeOut);
} else {
// The test case doesn't use inject(). ie `it('test', (done) => { ... }));`
jsmFn(name, testFn, timeOut);
}
} else {
// Asynchronous test function that takes "done" as parameter.
fn(done);
}
};
}
function _it(jsmFn: Function, name: string, testFn: Function, testTimeOut: number): void {
jsmFn(name, _wrapTestFn(testFn), testTimeOut);
}
/**
@ -165,27 +132,8 @@ function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | An
*
* {@example testing/ts/testing.ts region='beforeEach'}
*/
export function beforeEach(fn: FunctionWithParamTokens | AnyTestFn): void {
if (fn instanceof FunctionWithParamTokens) {
// The test case uses inject(). ie `beforeEach(inject([ClassA], (a) => { ...
// }));`
let fnT = fn;
jsmBeforeEach((done) => {
if (fnT.isAsync) {
runInAsyncTestZone(() => testInjector.execute(fnT), done, done.fail, 'beforeEach');
} else {
testInjector.execute(fnT);
done();
}
});
} else {
// The test case doesn't use inject(). ie `beforeEach((done) => { ... }));`
if ((<any>fn).length === 0) {
jsmBeforeEach(() => { (<SyncTestFn>fn)(); });
} else {
jsmBeforeEach((done) => { (<AsyncTestFn>fn)(done); });
}
}
export function beforeEach(fn: Function): void {
jsmBeforeEach(_wrapTestFn(fn));
}
/**
@ -200,8 +148,7 @@ export function beforeEach(fn: FunctionWithParamTokens | AnyTestFn): void {
*
* {@example testing/ts/testing.ts region='describeIt'}
*/
export function it(name: string, fn: FunctionWithParamTokens | AnyTestFn,
timeOut: number = null): void {
export function it(name: string, fn: Function, timeOut: number = null): void {
return _it(jsmIt, name, fn, timeOut);
}
@ -216,16 +163,14 @@ export function it(name: string, fn: FunctionWithParamTokens | AnyTestFn,
*
* {@example testing/ts/testing.ts region='xit'}
*/
export function xit(name: string, fn: FunctionWithParamTokens | AnyTestFn,
timeOut: number = null): void {
export function xit(name: string, fn: Function, timeOut: number = null): void {
return _it(jsmXIt, name, fn, timeOut);
}
/**
* See {@link fit}.
*/
export function iit(name: string, fn: FunctionWithParamTokens | AnyTestFn,
timeOut: number = null): void {
export function iit(name: string, fn: Function, timeOut: number = null): void {
return _it(jsmIIt, name, fn, timeOut);
}
@ -239,7 +184,6 @@ export function iit(name: string, fn: FunctionWithParamTokens | AnyTestFn,
*
* {@example testing/ts/testing.ts region='fit'}
*/
export function fit(name: string, fn: FunctionWithParamTokens | AnyTestFn,
timeOut: number = null): void {
export function fit(name: string, fn: Function, timeOut: number = null): void {
return _it(jsmIIt, name, fn, timeOut);
}

View File

@ -1,12 +1,13 @@
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {global, isFunction, Math} from 'angular2/src/facade/lang';
import {global, isPromise, Math} from 'angular2/src/facade/lang';
import {provide} from 'angular2/core';
import {getTestInjector, FunctionWithParamTokens, inject} from './test_injector';
import {AsyncTestCompleter} from './async_test_completer';
import {getTestInjector, inject} from './test_injector';
import {browserDetection} from './utils';
import {NgZone} from 'angular2/src/core/zone/ng_zone';
export {AsyncTestCompleter} from './async_test_completer';
export {inject} from './test_injector';
export {expect, NgMatchers} from './matchers';
@ -17,19 +18,6 @@ var _global = <any>(typeof window === 'undefined' ? global : window);
export var afterEach: Function = _global.afterEach;
export type SyncTestFn = () => void;
type AsyncTestFn = (done: () => void) => void;
type AnyTestFn = SyncTestFn | AsyncTestFn;
/**
* Injectable completer that allows signaling completion of an asynchronous test. Used internally.
*/
export class AsyncTestCompleter {
constructor(private _done: Function) {}
done(): void { this._done(); }
}
var jsmBeforeEach = _global.beforeEach;
var jsmDescribe = _global.describe;
var jsmDDescribe = _global.fdescribe;
@ -51,18 +39,15 @@ var testInjector = getTestInjector();
* Note: Jasmine own `beforeEach` is used by this library to handle DI providers.
*/
class BeforeEachRunner {
private _fns: Array<FunctionWithParamTokens | SyncTestFn> = [];
private _fns: Array<Function> = [];
constructor(private _parent: BeforeEachRunner) {}
beforeEach(fn: FunctionWithParamTokens | SyncTestFn): void { this._fns.push(fn); }
beforeEach(fn: Function): void { this._fns.push(fn); }
run(): void {
if (this._parent) this._parent.run();
this._fns.forEach((fn) => {
return isFunction(fn) ? (<SyncTestFn>fn)() :
(testInjector.execute(<FunctionWithParamTokens>fn));
});
this._fns.forEach((fn) => { fn(); });
}
}
@ -90,13 +75,13 @@ export function xdescribe(...args): void {
return _describe(jsmXDescribe, ...args);
}
export function beforeEach(fn: FunctionWithParamTokens | SyncTestFn): void {
export function beforeEach(fn: Function): void {
if (runnerStack.length > 0) {
// Inside a describe block, beforeEach() uses a BeforeEachRunner
runnerStack[runnerStack.length - 1].beforeEach(fn);
} else {
// Top level beforeEach() are delegated to jasmine
jsmBeforeEach(<SyncTestFn>fn);
jsmBeforeEach(fn);
}
}
@ -127,55 +112,37 @@ export function beforeEachBindings(fn): void {
beforeEachProviders(fn);
}
function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | AnyTestFn,
testTimeOut: number): void {
function _it(jsmFn: Function, name: string, testFn: Function, testTimeOut: number): void {
var runner = runnerStack[runnerStack.length - 1];
var timeOut = Math.max(globalTimeOut, testTimeOut);
if (testFn instanceof FunctionWithParamTokens) {
// The test case uses inject(). ie `it('test', inject([AsyncTestCompleter], (async) => { ...
// }));`
let testFnT = testFn;
jsmFn(name, (done) => {
var completerProvider = provide(AsyncTestCompleter, {
useFactory: () => {
// Mark the test as async when an AsyncTestCompleter is injected in an it()
if (!inIt) throw new Error('AsyncTestCompleter can only be injected in an "it()"');
return new AsyncTestCompleter();
}
});
testInjector.addProviders([completerProvider]);
runner.run();
if (testFn.hasToken(AsyncTestCompleter)) {
jsmFn(name, (done) => {
var completerProvider = provide(AsyncTestCompleter, {
useFactory: () => {
// Mark the test as async when an AsyncTestCompleter is injected in an it()
if (!inIt) throw new Error('AsyncTestCompleter can only be injected in an "it()"');
return new AsyncTestCompleter(done);
}
});
testInjector.addProviders([completerProvider]);
runner.run();
inIt = true;
testInjector.execute(testFnT);
inIt = false;
}, timeOut);
inIt = true;
if (testFn.length == 0) {
let retVal = testFn();
if (isPromise(retVal)) {
// Asynchronous test function that returns a Promise - wait for completion.
(<Promise<any>>retVal).then(done, done.fail);
} else {
// Synchronous test function - complete immediately.
done();
}
} else {
jsmFn(name, () => {
runner.run();
testInjector.execute(testFnT);
}, timeOut);
// Asynchronous test function that takes in 'done' parameter.
testFn(done);
}
} else {
// The test case doesn't use inject(). ie `it('test', (done) => { ... }));`
if ((<any>testFn).length === 0) {
jsmFn(name, () => {
runner.run();
(<SyncTestFn>testFn)();
}, timeOut);
} else {
jsmFn(name, (done) => {
runner.run();
(<AsyncTestFn>testFn)(done);
}, timeOut);
}
}
inIt = false;
}, timeOut);
}
export function it(name, fn, timeOut = null): void {
@ -190,7 +157,6 @@ export function iit(name, fn, timeOut = null): void {
return _it(jsmIIt, name, fn, timeOut);
}
export interface GuinessCompatibleSpy extends jasmine.Spy {
/** By chaining the spy with and.returnValue, all calls to the function will return a specific
* value. */

View File

@ -26,31 +26,18 @@ import 'package:angular2/src/core/reflection/reflection_capabilities.dart';
import 'package:angular2/src/core/di/provider.dart' show bind;
import 'package:angular2/src/facade/collection.dart' show StringMapWrapper;
import 'async_test_completer.dart';
export 'async_test_completer.dart' show AsyncTestCompleter;
import 'test_injector.dart';
export 'test_injector.dart' show inject;
TestInjector _testInjector = getTestInjector();
bool _isCurrentTestAsync;
Future _currentTestFuture;
bool _inIt = false;
bool _initialized = false;
List<dynamic> _platformProviders = [];
List<dynamic> _applicationProviders = [];
class AsyncTestCompleter {
final _completer = new Completer();
AsyncTestCompleter() {
_currentTestFuture = this.future;
}
void done() {
_completer.complete();
}
Future get future => _completer.future;
}
void setDartBaseTestProviders(List<dynamic> platform, List<dynamic> application) {
_platformProviders = platform;
_applicationProviders = application;
@ -70,18 +57,15 @@ void testSetup() {
gns.beforeEach(() {
_testInjector.reset();
_currentTestFuture = null;
});
var completerProvider = bind(AsyncTestCompleter).toFactory(() {
// Mark the test as async when an AsyncTestCompleter is injected in an it(),
if (!_inIt) throw 'AsyncTestCompleter can only be injected in an "it()"';
_isCurrentTestAsync = true;
return new AsyncTestCompleter();
});
gns.beforeEach(() {
_isCurrentTestAsync = false;
_testInjector.addProviders([completerProvider]);
});
}
@ -113,22 +97,16 @@ void beforeEachBindings(Function fn) {
void beforeEach(fn) {
testSetup();
if (fn is! FunctionWithParamTokens) fn =
new FunctionWithParamTokens([], fn, false);
gns.beforeEach(() {
_testInjector.execute(fn);
});
gns.beforeEach(fn);
}
void _it(gnsFn, name, fn) {
testSetup();
if (fn is! FunctionWithParamTokens) fn =
new FunctionWithParamTokens([], fn, false);
gnsFn(name, () {
_inIt = true;
_testInjector.execute(fn);
var retVal = fn();
_inIt = false;
if (_isCurrentTestAsync) return _currentTestFuture;
return retVal;
});
}

View File

@ -150,6 +150,19 @@ export function main() {
expect(value).toEqual('async value');
})));
it('should allow use of "done"', (done) => {
inject([FancyService], (service) => {
let count = 0;
let id = setInterval(() => {
count++;
if (count > 2) {
clearInterval(id);
done();
}
}, 5);
})(); // inject needs to be invoked explicitly with ().
});
describe('using beforeEach', () => {
beforeEach(inject([FancyService],
(service) => { service.value = 'value modified in beforeEach'; }));
@ -174,6 +187,15 @@ export function main() {
withProviders(() => [bind(FancyService).toValue(new FancyService())])
.inject([FancyService],
(service) => { expect(service.value).toEqual('real value'); }));
it('should return value from inject', () => {
let retval = withProviders(() => [bind(FancyService).toValue(new FancyService())])
.inject([FancyService], (service) => {
expect(service.value).toEqual('real value');
return 10;
})();
expect(retval).toBe(10);
});
});
});

View File

@ -79,8 +79,7 @@ dynamic _runInjectableFunction(Function fn) {
tokens.add(token);
}
var injectFn = new FunctionWithParamTokens(tokens, fn, false);
return _testInjector.execute(injectFn);
return _testInjector.execute(tokens, fn);
}
/// Use the test injector to get bindings and run a function.