test(core): refactor ApplicationInitStatus tests to avoid TestBed side-effects (#33222)

Currently TestBed (both ViewEngine and Ivy) invoke `ApplicationInitStatus.runInitializers` as a part of the
bootstrap process to mimic real bootstrap steps. This is problematic for the `ApplicationInitStatus` class
tests since the `runInitializers` call performed by TestBed interfere with actual tests.

This commit updates ApplicationInitStatus tests to interact with the class directly instead of relying on TestBed
APIs to retrieve the class though DI.

PR Close #33222
This commit is contained in:
Andrew Kushnir 2021-02-18 15:52:13 -08:00 committed by atscott
parent ca17ac523c
commit 995adb2297
1 changed files with 143 additions and 164 deletions

View File

@ -5,175 +5,154 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {APP_INITIALIZER, ApplicationInitStatus} from '@angular/core/src/application_init';
import {Observable, Subscriber} from 'rxjs';
import {ApplicationInitStatus} from '@angular/core/src/application_init';
import {EMPTY, Observable, Subscriber} from 'rxjs';
import {inject, TestBed, waitForAsync} from '../testing';
describe('ApplicationInitStatus', () => {
let status: ApplicationInitStatus;
const runInitializers = () =>
// Cast to `any` to access an internal function for testing purposes.
(status as any).runInitializers();
{
describe('ApplicationInitStatus', () => {
describe('no initializers', () => {
it('should return true for `done`',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
(status as any).runInitializers();
expect(status.done).toBe(true);
})));
it('should return a promise that resolves immediately for `donePromise`',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
(status as any).runInitializers();
status.donePromise.then(() => {
expect(status.done).toBe(true);
});
})));
describe('no initializers', () => {
beforeEach(() => {
status = new ApplicationInitStatus([]);
});
describe('with async promise initializers', () => {
let resolve: (result: any) => void;
let reject: (reason?: any) => void;
let promise: Promise<any>;
let initFnInvoked = false;
beforeEach(() => {
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
TestBed.configureTestingModule({
providers: [
{provide: APP_INITIALIZER, multi: true, useValue: () => promise},
]
});
});
it('should update the status once all async promise initializers are done',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
setTimeout(() => {
initFnInvoked = true;
resolve(null);
});
expect(status.done).toBe(false);
status.donePromise.then(() => {
expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true);
});
})));
it('should handle a case when promise is rejected',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
setTimeout(() => {
initFnInvoked = true;
reject();
});
expect(status.done).toBe(false);
status.donePromise
.then(
() => fail('`donePromise.then` should not be invoked when promise is rejected'))
.catch(() => {
expect(status.done).toBe(false);
expect(initFnInvoked).toBe(true);
});
})));
it('should return true for `done`', () => {
runInitializers();
expect(status.done).toBe(true);
});
describe('with app initializers represented using observables', () => {
let subscriber: Subscriber<any>;
let observable: Observable<any>;
let initFnInvoked = false;
beforeEach(() => {
observable = new Observable((res) => {
subscriber = res;
});
TestBed.configureTestingModule({
providers: [
{provide: APP_INITIALIZER, multi: true, useValue: () => observable},
]
});
});
it('should update the status once all async observable initializers are completed',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
setTimeout(() => {
initFnInvoked = true;
subscriber.complete();
});
expect(status.done).toBe(false);
status.donePromise.then(() => {
expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true);
});
})));
it('should update the status once all async observable initializers nexted and completed',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
subscriber.next('one');
subscriber.next('two');
setTimeout(() => {
initFnInvoked = true;
subscriber.complete();
});
expect(status.done).toBe(false);
status.donePromise.then(() => {
expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true);
});
})));
it('should update the status if all async observable initializers are completed before runInitializers',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
subscriber.complete();
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
expect(status.done).toBe(false);
status.donePromise.then(() => {
expect(status.done).toBe(true);
});
})));
it('should handle a case when observable emits an error',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
setTimeout(() => {
initFnInvoked = true;
subscriber.error();
});
expect(status.done).toBe(false);
status.donePromise
.then(
() => fail(
'`donePromise.then` should not be invoked when observable emits an error'))
.catch(() => {
expect(status.done).toBe(false);
expect(initFnInvoked).toBe(true);
});
})));
it('should return a promise that resolves immediately for `donePromise`', async () => {
runInitializers();
await status.donePromise;
expect(status.done).toBe(true);
});
});
}
describe('with async promise initializers', () => {
let resolve: (result: any) => void;
let reject: (reason?: any) => void;
let promise: Promise<any>;
let initFnInvoked = false;
beforeEach(() => {
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
status = new ApplicationInitStatus([() => promise]);
});
it('should update the status once all async promise initializers are done', async () => {
runInitializers();
setTimeout(() => {
initFnInvoked = true;
resolve(null);
});
expect(status.done).toBe(false);
await status.donePromise;
expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true);
});
it('should handle a case when promise is rejected', async () => {
runInitializers();
setTimeout(() => {
initFnInvoked = true;
reject();
});
expect(status.done).toBe(false);
try {
await status.donePromise;
fail('donePromise should have been rejected when promise is rejected');
} catch {
expect(status.done).toBe(false);
expect(initFnInvoked).toBe(true);
}
});
});
describe('with app initializers represented using observables', () => {
let subscriber: Subscriber<any>;
let initFnInvoked = false;
beforeEach(() => {
const observable = new Observable((res) => {
subscriber = res;
});
status = new ApplicationInitStatus([() => observable]);
});
it('should update the status once all async observable initializers are completed',
async () => {
runInitializers();
setTimeout(() => {
initFnInvoked = true;
subscriber.complete();
});
expect(status.done).toBe(false);
await status.donePromise;
expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true);
});
it('should update the status once all async observable initializers emitted and completed',
async () => {
runInitializers();
subscriber.next('one');
subscriber.next('two');
setTimeout(() => {
initFnInvoked = true;
subscriber.complete();
});
await status.donePromise;
expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true);
});
it('should update the status if all async observable initializers are completed synchronously',
async () => {
// Create a status instance using an initializer that returns the `EMPTY` Observable
// which completes synchronously upon subscription.
status = new ApplicationInitStatus([() => EMPTY]);
runInitializers();
// Although the Observable completes synchronously, we still queue a promise for
// simplicity. This means that the `done` flag will not be `true` immediately, even
// though there was not actually any asynchronous activity.
expect(status.done).toBe(false);
await status.donePromise;
expect(status.done).toBe(true);
});
it('should handle a case when observable emits an error', async () => {
runInitializers();
setTimeout(() => {
initFnInvoked = true;
subscriber.error();
});
expect(status.done).toBe(false);
try {
await status.donePromise;
fail('donePromise should have been rejected when observable emits an error');
} catch {
expect(status.done).toBe(false);
expect(initFnInvoked).toBe(true);
}
});
});
});