FakeAsync - Asynchronous Work (Jasmine) part 2

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:

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);
  <span class="code-annotation">tick(2000);</span>
  fixture.detectChanges();
  let value = debugElement.query(By.css('h1')).nativeElement.innerText;
  expect(value).toEqual('0'); // value should still be 0 after 2 seconds

  <span class="code-annotation">tick(3000)</span>;
  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');
}));

Or it can also be like this:

it('Async test example - setTimeout() with flush()', fakeAsync(() => {
    let test = false;
    setTimeout(() => {});
    setTimeout(() => {
        test = true;
        expect(test).toBeTruthy();
    }, 1000);
    flush();
}));

Last updated