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,27 +5,30 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {APP_INITIALIZER, ApplicationInitStatus} from '@angular/core/src/application_init'; import {ApplicationInitStatus} from '@angular/core/src/application_init';
import {Observable, Subscriber} from 'rxjs'; import {EMPTY, Observable, Subscriber} from 'rxjs';
import {inject, TestBed, waitForAsync} from '../testing';
{
describe('ApplicationInitStatus', () => { describe('ApplicationInitStatus', () => {
describe('no initializers', () => { let status: ApplicationInitStatus;
it('should return true for `done`', const runInitializers = () =>
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { // Cast to `any` to access an internal function for testing purposes.
(status as any).runInitializers(); (status as any).runInitializers();
expect(status.done).toBe(true);
})));
it('should return a promise that resolves immediately for `donePromise`', describe('no initializers', () => {
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { beforeEach(() => {
(status as any).runInitializers(); status = new ApplicationInitStatus([]);
status.donePromise.then(() => { });
it('should return true for `done`', () => {
runInitializers();
expect(status.done).toBe(true);
});
it('should return a promise that resolves immediately for `donePromise`', async () => {
runInitializers();
await status.donePromise;
expect(status.done).toBe(true); expect(status.done).toBe(true);
}); });
})));
}); });
describe('with async promise initializers', () => { describe('with async promise initializers', () => {
@ -33,23 +36,17 @@ import {inject, TestBed, waitForAsync} from '../testing';
let reject: (reason?: any) => void; let reject: (reason?: any) => void;
let promise: Promise<any>; let promise: Promise<any>;
let initFnInvoked = false; let initFnInvoked = false;
beforeEach(() => { beforeEach(() => {
promise = new Promise((res, rej) => { promise = new Promise((res, rej) => {
resolve = res; resolve = res;
reject = rej; reject = rej;
}); });
TestBed.configureTestingModule({ status = new ApplicationInitStatus([() => promise]);
providers: [
{provide: APP_INITIALIZER, multi: true, useValue: () => promise},
]
});
}); });
it('should update the status once all async promise initializers are done', it('should update the status once all async promise initializers are done', async () => {
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { runInitializers();
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
setTimeout(() => { setTimeout(() => {
initFnInvoked = true; initFnInvoked = true;
@ -57,17 +54,13 @@ import {inject, TestBed, waitForAsync} from '../testing';
}); });
expect(status.done).toBe(false); expect(status.done).toBe(false);
status.donePromise.then(() => { await status.donePromise;
expect(status.done).toBe(true); expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true); expect(initFnInvoked).toBe(true);
}); });
})));
it('should handle a case when promise is rejected', it('should handle a case when promise is rejected', async () => {
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { runInitializers();
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
setTimeout(() => { setTimeout(() => {
initFnInvoked = true; initFnInvoked = true;
@ -75,36 +68,29 @@ import {inject, TestBed, waitForAsync} from '../testing';
}); });
expect(status.done).toBe(false); expect(status.done).toBe(false);
status.donePromise try {
.then( await status.donePromise;
() => fail('`donePromise.then` should not be invoked when promise is rejected')) fail('donePromise should have been rejected when promise is rejected');
.catch(() => { } catch {
expect(status.done).toBe(false); expect(status.done).toBe(false);
expect(initFnInvoked).toBe(true); expect(initFnInvoked).toBe(true);
}
}); });
})));
}); });
describe('with app initializers represented using observables', () => { describe('with app initializers represented using observables', () => {
let subscriber: Subscriber<any>; let subscriber: Subscriber<any>;
let observable: Observable<any>;
let initFnInvoked = false; let initFnInvoked = false;
beforeEach(() => { beforeEach(() => {
observable = new Observable((res) => { const observable = new Observable((res) => {
subscriber = res; subscriber = res;
}); });
TestBed.configureTestingModule({ status = new ApplicationInitStatus([() => observable]);
providers: [
{provide: APP_INITIALIZER, multi: true, useValue: () => observable},
]
});
}); });
it('should update the status once all async observable initializers are completed', it('should update the status once all async observable initializers are completed',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { async () => {
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class runInitializers();
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
setTimeout(() => { setTimeout(() => {
initFnInvoked = true; initFnInvoked = true;
@ -112,17 +98,14 @@ import {inject, TestBed, waitForAsync} from '../testing';
}); });
expect(status.done).toBe(false); expect(status.done).toBe(false);
status.donePromise.then(() => { await status.donePromise;
expect(status.done).toBe(true); expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true); expect(initFnInvoked).toBe(true);
}); });
})));
it('should update the status once all async observable initializers nexted and completed', it('should update the status once all async observable initializers emitted and completed',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { async () => {
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class runInitializers();
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
subscriber.next('one'); subscriber.next('one');
subscriber.next('two'); subscriber.next('two');
@ -132,32 +115,30 @@ import {inject, TestBed, waitForAsync} from '../testing';
subscriber.complete(); subscriber.complete();
}); });
expect(status.done).toBe(false); await status.donePromise;
status.donePromise.then(() => {
expect(status.done).toBe(true); expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true); expect(initFnInvoked).toBe(true);
}); });
})));
it('should update the status if all async observable initializers are completed before runInitializers', it('should update the status if all async observable initializers are completed synchronously',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { async () => {
subscriber.complete(); // Create a status instance using an initializer that returns the `EMPTY` Observable
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class // which completes synchronously upon subscription.
// instance for testing purposes to invoke initializer functions. status = new ApplicationInitStatus([() => EMPTY]);
(status as any).runInitializers();
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); expect(status.done).toBe(false);
status.donePromise.then(() => { await status.donePromise;
expect(status.done).toBe(true); expect(status.done).toBe(true);
}); });
})));
it('should handle a case when observable emits an error', it('should handle a case when observable emits an error', async () => {
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { runInitializers();
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
setTimeout(() => { setTimeout(() => {
initFnInvoked = true; initFnInvoked = true;
@ -165,15 +146,13 @@ import {inject, TestBed, waitForAsync} from '../testing';
}); });
expect(status.done).toBe(false); expect(status.done).toBe(false);
status.donePromise try {
.then( await status.donePromise;
() => fail( fail('donePromise should have been rejected when observable emits an error');
'`donePromise.then` should not be invoked when observable emits an error')) } catch {
.catch(() => {
expect(status.done).toBe(false); expect(status.done).toBe(false);
expect(initFnInvoked).toBe(true); expect(initFnInvoked).toBe(true);
});
})));
});
});
} }
});
});
});