Tutorial

How To Use waitForAsync and fakeAsync with Angular Testing

Updated on July 7, 2021
Default avatar

By Alligator.io

How To Use waitForAsync and fakeAsync with Angular Testing

Introduction

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.

Prerequisites

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.

Setting Up the Project

First, use @angular/cli to create a new project:

  1. ng new angular-async-fakeasync-example

Then, navigate to the newly created project directory:

  1. 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.

Testing with 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:

src/app/app.component.ts
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:

src/app/app.component.html
<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:

src/app/app.component.spec.ts
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:

  1. ng test

This will produce a successful 'should display title' test result.

Testing with 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:

src/app/app.component.html
<h1>
  {{ incrementDecrement.value }}
</h1>

<button (click)="increment()" class="increment">
  Increment
</button>

It calls an increment method in the component class that looks like this:

src/app/app.component.ts
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:

  1. ng generate service increment-decrement

That has an increment method that’s made asynchronous with the use of a setTimeout:

src/app/increment-decrement.service.ts
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:

src/app/app.component.spec.ts
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:

  1. 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:

src/app/app.component.spec.ts
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:

  1. ng test

This will produce a successful 'should increment in template' test result.

Conclusion

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.

Learn more about us


About the authors
Default avatar
Alligator.io

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
1 Comments


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

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel