Angular fakeAsync and RxJS timer

Testing RxJS async logic in Angular, using fakeAsync

RxJS is a library for reactive programming, It provides a set of tools for dealing with asynchronous programming, handling events, and managing data streams. Timer is an operator that creates a stream which emits at a given interval with an optional delay.

Let's say have we have a situation where we are polling for some data, and we use the RxJS timer for that. The component code should be similar to this:

import { Observable, of, Subject, switchMap, takeUntil, timer } from 'rxjs';

import { Component, OnDestroy, OnInit } from '@angular/core';

import { AppService } from './app.service';
import { Room } from './models/room.type';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit, OnDestroy {
  public availableRooms$: Observable<Room[]> = of([]);

  private readonly REFRESH_RATE = 10000;
  private readonly destroy$ = new Subject<void>();

  constructor(private appService: AppService) {}

  ngOnInit() {
    this.availableRooms$ = timer(0, this.REFRESH_RATE).pipe(
      takeUntil(this.destroy$),
      switchMap(() => this.appService.getRooms())
    );
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

The typical Angular test, using TestBed, should look somewhat like this:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { AppComponent } from './app.component';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule],
      declarations: [AppComponent]
    });

    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create the component', () => {
    expect(component).toBeTruthy();
  });
});

In order to test the polling functionality of our application we need a new test. To mock the time and check that the query triggered at every refresh we can use the fakeAsync feature together with tick. We also need to mock and spy on the service method.

import { of } from 'rxjs';

import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { AppComponent } from './app.component';
import { AppService } from './app.service';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let component: AppComponent;
  let appService: jasmine.SpyObj<AppService>;

  beforeEach(() => {
    appService = jasmine.createSpyObj('AppService', ['getRooms']);

    TestBed.configureTestingModule({
      imports: [RouterTestingModule],
      declarations: [AppComponent],
      providers: [
        {
          provide: AppService,
          useValue: appService,
        },
      ]
    });

    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create the app', () => {
    expect(component).toBeTruthy();
  });

  it('should call the service on refresh', fakeAsync(() => {
    const REFRESH_RATE = 10000;
    appService.getRooms.and.returnValue(of([]));

    tick();
    tick(REFRESH_RATE);
    tick(REFRESH_RATE);

    expect(appService.getRooms).toHaveBeenCalledTimes(3);
  }));
});

In theory this should work, we call detectChanges in our beforeEach, so the ngOnInit is called, but the test seems to be failing with the following error:

Expected spy AppService.getRooms to have been called 3 times. It was called 0 times.

A key thing that needs to be done when working with fakeAsync and tick is to move detectChanges inside the test, only then it will run as intended.

  it('should call the service on refresh', fakeAsync(() => {
    const REFRESH_RATE = 10000;
    appService.getRooms.and.returnValue(of([]));
    fixture.detectChanges();

    tick();
    tick(REFRESH_RATE);
    tick(REFRESH_RATE);

    expect(appService.getRooms).toHaveBeenCalledTimes(3);
  }));

However, taking into consideration that we are working with a timer, we get the following error:

Error: 1 periodic timer(s) still in the queue.

This happens because the stream is not closed. We can either trigger the action that would complete the stream, the destroy lifecycle step in our case, or call the discardPeriodicTasks method. The final test should look similar to this:

  it('should call the service on refresh', fakeAsync(() => {
    const REFRESH_RATE = 10000;
    appService.getRooms.and.returnValue(of([]));
    fixture.detectChanges();

    tick();
    tick(REFRESH_RATE);
    tick(REFRESH_RATE);

    discardPeriodicTasks();
    expect(appService.getRooms).toHaveBeenCalledTimes(3);
  }));

Now the test passes and asserts that our service method is called every time the timer is triggered.