Angular 2+ provides async
and fakeAsync
utilities for testing asynchronous code. This should make your Angular unit and integration tests that much easier to write.
In this article, you will be introduced to waitForAsync
and fakeAsync
with sample tests.
To complete this tutorial, you will need:
This tutorial was verified with Node v16.4.0, npm
v7.19.0, and @angular/core
v12.1.1.
First, use @angular/cli
to create a new project:
- ng new angular-async-fakeasync-example
Then, navigate to the newly created project directory:
- cd angular-async-fakeasync-example
This will create a new Angular project with app.component.html
, app.compontent.ts
, and app.component.spec.ts
files.
waitForAsync
The waitForAsync
utility tells Angular to run the code in a dedicated test zone that intercepts promises. We briefly covered the async utility in our intro to unit testing in Angular when using compileComponents
.
The whenStable
utility allows us to wait until all promises have been resolved to run our expectations.
First open app.component.ts
and use a Promise
to resolve
the title
:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title!: string;
setTitle() {
new Promise(resolve => {
resolve('Async Title!');
}).then((val: any) => {
this.title = val;
});
}
}
Then open app.component.html
and replace it with a h1
and button
:
<h1>
{{ title }}
</h1>
<button (click)="setTitle()" class="set-title">
Set Title
</button>
When the button is clicked, the title
property is set using a promise.
And here’s how we can test this functionality using waitForAsync
and whenStable
:
import { TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
});
it('should display title', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.debugElement
.query(By.css('.set-title'))
.triggerEventHandler('click', null);
fixture.whenStable().then(() => {
fixture.detectChanges();
const value = fixture.debugElement
.query(By.css('h1'))
.nativeElement
.innerText;
expect(value).toEqual('Async Title!');
});
}));
});
Note: In a real app you will have promises that actually wait on something useful like a response from a request to your backend API.
At this point, you can run your test:
- ng test
This will produce a successful 'should display title'
test result.
fakeAsync
The problem with async
is that we still have to introduce real waiting in our tests, and this can make our tests very slow. fakeAsync
comes to the rescue and helps to test asynchronous code in a synchronous way.
To demonstrate fakeAsync
, let’s start with a simple example. Say our component template has a button that increments a value like this:
<h1>
{{ incrementDecrement.value }}
</h1>
<button (click)="increment()" class="increment">
Increment
</button>
It calls an increment
method in the component class that looks like this:
import { Component } from '@angular/core';
import { IncrementDecrementService } from './increment-decrement.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(public incrementDecrement: IncrementDecrementService) { }
increment() {
this.incrementDecrement.increment();
}
}
And this method itself calls a method in an incrementDecrement
service:
- ng generate service increment-decrement
That has an increment
method that’s made asynchronous with the use of a setTimeout
:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class IncrementDecrementService {
value = 0;
message!: string;
increment() {
setTimeout(() => {
if (this.value < 15) {
this.value += 1;
this.message = '';
} else {
this.message = 'Maximum reached!';
}
}, 5000); // wait 5 seconds to increment the value
}
}
Obviously, in a real-world app this asynchronicity can be introduced in a number of different ways.
Let’s now use fakeAsync
with the tick
utility to run an integration test and make sure the value is incremented in the template:
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
]
}).compileComponents();
});
it('should increment in template after 5 seconds', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
tick(2000);
fixture.detectChanges();
const value1 = fixture.debugElement.query(By.css('h1')).nativeElement.innerText;
expect(value1).toEqual('0'); // value should still be 0 after 2 seconds
tick(3000);
fixture.detectChanges();
const value2 = fixture.debugElement.query(By.css('h1')).nativeElement.innerText;
expect(value2).toEqual('1'); // 3 seconds later, our value should now be 1
}));
});
Notice how the tick
utility is used inside a fakeAsync
block to simulate the passage of time. The argument passed-in to tick
is the number of milliseconds to pass, and these are cumulative within a test.
Note: Tick
can also be used with no argument, in which case it waits until all the microtasks are done (when promises are resolved for example).
At this point, you can run your test:
- ng test
This will produce a successful 'should increment in template after 5 seconds'
test result.
Specifying the passing time like that can quickly become cumbersome, and can become a problem when you don’t know how much time should pass.
A new utility called flush
was introduced in Angular 4.2 and helps with that issue. It simulates the passage of time until the macrotask queue is empty. Macrotasks include things like setTimouts
, setIntervals
, and requestAnimationFrame
.
So, using flush
, we can write a test like this for example:
import { TestBed, fakeAsync, flush } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
]
}).compileComponents();
});
it('should increment in template', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
flush();
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('h1')).nativeElement.innerText;
expect(value).toEqual('1');
}));
});
At this point, you can run your test:
- ng test
This will produce a successful 'should increment in template'
test result.
In this article, you were introduced to waitForAsync
and fakeAsync
with sample tests.
You can also refer to the official documentation for an in-depth Angular testing guide.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Thank you, that was helpful