2

How To Use waitForAsync and fakeAsync with Angular Testing

 2 years ago
source link: https://www.digitalocean.com/community/tutorials/angular-testing-async-fakeasync
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

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.

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:

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:

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:

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:

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK