Angular Testing: async and fakeAsync

Testing asynchronous code has always been a challenge, but it’s now easier than ever, thanks to the async and fakeAsync utilities available for Angular 2+. This should make your Angular unit and integration tests that much easier to write.

Async

The async 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, so let’s go over one more detail here; namely the use of async with whenStable. whenStable allows us to wait until all promises have been resolved to run our expectations. Let’s start with an example component like this:

import { Component } from '@angular/core';


@Component({
  selector: 'app-root',
  template: `<h1<{{ title }}</h1>

  <button (click)="setTitle()" class="set-title">
    set title
  </button>
  `
})
export class AppComponent {
  title: string;

  setTitle() {
    new Promise(resolve => {
      resolve('One crazy app!');
    }).then((val: string) => {
      this.title = val;
    });
  }
}

When the button is clicked, the title property is set using a promise. And here’s how we can test this functionality using async and whenStable:

// ...our imports here

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let debugElement: DebugElement;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
      providers: [IncrementDecrementService]
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    debugElement = fixture.debugElement;
  }));

  it('should display title', async(() => {
    debugElement
      .query(By.css('.set-title'))
      .triggerEventHandler('click', null);

    fixture.whenStable().then(() => {
      fixture.detectChanges();
      const value = debugElement.query(By.css('h1')).nativeElement.innerText;
      expect(value).toEqual('One crazy app!');
    });
  }));
});

Obviously in a real app you'll have promises that actually wait on something useful like a response from a request to your backend API.

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:

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:

app.component.ts

increment() {
  this.incrementDecrement.increment();
}

And this method itself calls a method in an incrementDecrement service that has an increment method that’s made asynchronous with the use of a setTimeout:

increment-decrement.service.ts

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:

app.component.spec.ts

import { TestBed, fakeAsync, tick, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { AppComponent } from './app.component';
import { IncrementDecrementService } from './increment-decrement.service';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let debugElement: DebugElement;

  beforeEach(
    async(() => {
      TestBed.configureTestingModule({
        declarations: [AppComponent],
        providers: [IncrementDecrementService]
      }).compileComponents();

      fixture = TestBed.createComponent(AppComponent);
      debugElement = fixture.debugElement;
    })
  );

  it('should increment in template after 5 seconds', fakeAsync(() => {
      debugElement
        .query(By.css('button.increment'))
        .triggerEventHandler('click', null);

      tick(2000);
      fixture.detectChanges();
      let value = debugElement.query(By.css('h1')).nativeElement.innerText;
      expect(value).toEqual('0'); // value should still be 0 after 2 seconds

      tick(3000);
      fixture.detectChanges();

      const value = debugElement.query(By.css('h1')).nativeElement.innerText;
      expect(value).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.

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

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 basically 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:

it('should increment in template', fakeAsync(() => {
  debugElement
    .query(By.css('button.increment'))
    .triggerEventHandler('click', null);

  flush();
  fixture.detectChanges();

  const value = debugElement.query(By.css('h1')).nativeElement.innerText;
  expect(value).toEqual('1');
}));
✖ Clear

🕵 Search Results

🔎 Searching...