Angular, Jest, and async NgOnInit

It is common to fetch data in Angular’s ngOnInit method with the expectation that the page will update when the data is returned and bound. In Typescript it is nice to use async methods to improve readability leading to components that have async ngOnInit() methods.

This presents a challenge when working with a testing framework, as we need to wait for promises to be resolved before our component is set up and tests can continue. To handle this, Angular’s TestBed includes async support, and in particular the whenStable() method for waiting for promises to resolve. This is useful, but means having a whenStable()/detectChanges() pair in every test method. Instead I found that whenStable() runs fine in the beforeEach() method where the fixture is created (via TestBed.createComponent()). I’m not clear on why this works when it’s not in an Angular async() (as opposed to a TypeScript async) block, with my best guess being that the TestBed.createComponent automatically creates the NgZone that whenStable() needs.

As the beforeEach is calling whenStable() it now needs to be (TypeScript) async and Jest will wait for the resulting promise to finish before it considers beforeEach to be done. With this approach the unit tests no longer need to be wrapped in an (Angular test) async.

The following code illustrates the full pattern, and also uses a mocking library, ts-jest. The code is all in TypeScript and uses (TypeScript) async for handling promises. It is organized so each inner describe block (e.g. ‘with specificMockDataset’) covers a specific test data set.

// --- MyComponent.Component.ts ---
// ... usual imports ...
export class MyComponent implements OnInit {
	async ngOnInit() {
		this.thing = await this.myService.getFromServer();
	}
}

// --- MyComponent.Component.test.ts ---
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { mocked } from 'ts-jest/utils';
import { MyComponent } from 'app/components/my.component';
import { MyService } from 'app/services/my.service';
import { AppModule } from 'app/app.module';

jest.mock('app/services/my.service');
const mockService = mocked(MyService, true);

describe('MyComponent component', () => {
    let fixture: ComponentFixture;

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            declarations: [MyComponent],
            providers: [mockService],
            imports: [AppModule]
        }).compileComponents();
    });

    describe('with specificMockDataset', () => {
		
		beforeEach(async () => {
            mockService.mockClear();
            // Mock the async service method getFromServer(). In theory this is the same as .mockResolvedValue() - but that fails and this works
            mockService.prototype.getFromServer.mockImplementation(() => { return Promise.resolve({ field: "value" }); });

            fixture = TestBed.createComponent(MyComponent);
            fixture.detectChanges(); // calls ngOnInit()
            await fixture.whenStable(); // waits for promises to complete
            fixture.detectChanges(); // detect changes made due to field changes made during ngOnInit
        });

        it('should have options', () => {
            expect(fixture.componentInstance).toBeDefined();
            const selectClient: HTMLSelectElement = fixture.debugElement.query(By.css('#someEle')).nativeElement;
            expect(selectClient.options.length).toBe(2);
        });
	});
});