Search code examples
angulartypescriptjasmine

Test Observable with fakeAsync, delay and tick using Jasmine


I have a pipe that helps return the state of an observable.

import {Pipe, PipeTransform} from '@angular/core';
import {Observable, of} from 'rxjs';
import {catchError, map, startWith} from 'rxjs/operators';

/** Specifies the status of an Observable. */
export interface ObservableStatus<T> {
  loading?: boolean;
  value?: T;
  error?: boolean;
}

/** Returns the status {@code ObservableStatus} of a given Observable. */
@Pipe({name: 'getObserverStatus'})
export class ObservableStatusPipe implements PipeTransform {
  transform<T = Item>(observer: Observable<T>):
      Observable<ObservableStatus<T>> {
    return observer.pipe(
        map((value: T) => {
          return {
            loading: false,
            error: false,
            value,
          };
        }),
        startWith({loading: true}),
        catchError(error => of({loading: false, error: true})));
  }
}

I want to write unit tests for this functionality using Jasmine. I tried using fakeAsync, delay, tick, flush, discardPeriodicTasks but it doesn't seem to work.

I have tried different ways:

  • Way 1
describe('loading test', () => {
  const loadingPipe = new ObservableStatusPipe();

  it('returns state of an observable', fakeAsync(() => {
    const input: Observable<Item> = of({name: 'Item'}).pipe(delay(1000));

    const result: Observable<ObservableStatus<Item>> = loadingPipe.transform(input);

    result.subscribe(val => {
      expect(val.loading).toEqual(true);
      expect(val.value).toBeUndefined();
    });
    tick(2000);
    result.subscribe(val => {
      expect(val.loading).toEqual(false);
      expect(val.value!.name).toEqual('Item');
    });
  }));
});

The above test case fails with following failures:

Error: Expected true to equal false. (at expect(val.loading).toEqual(false))
Error: 1 periodic timer(s) still in the queue.
  • Way 2: From online, I saw that we can use flush to flush any pending tasks.
describe('loading test', () => {
  const loadingPipe = new ObservableStatusPipe();

  it('returns state of an observable', fakeAsync(() => {
    const input: Observable<Item> = of({name: 'Item'}).pipe(delay(1000));

    const result: Observable<ObservableStatus<Item>> = loadingPipe.transform(input);

    result.subscribe(val => {
      expect(val.loading).toEqual(true);
      expect(val.value).toBeUndefined();
    });
    tick(2000);
    result.subscribe(val => {
      expect(val.loading).toEqual(false);
      expect(val.value!.name).toEqual('Item');
    });
    flush();     // <----- here.
  }));
});

This is helping to resolve Error: 1 periodic timer(s) still in the queue. issue. However the test case still fails with:

Error: Expected true to equal false.
TypeError: Cannot read properties of undefined (reading 'name')

Does all this mean the tick is somehow not simulating time on input observable?

  • I tested the same simulation on input observable directly:
describe('loading test', () => {
  const loadingPipe = new ObservableStatusPipe();

  it('returns state of an observable', fakeAsync(() => {
    const input: Observable<Item> = of({name: 'Item'}).pipe(delay(1000));

    input.subscribe(val => {
      expect(val.name).toBeUndefined();
    });
    tick(2000);
    input.subscribe(val => {
      expect(val.name).toEqual('Item');
    });
    discardPeriodicTasks();    <--- Using flush() here is causing 'Error: 2 periodic timer(s) still in the queue' error.
  }));
});

The above test case is passing. But I am still confused why flush() is not working here.

describe('loading test', () => {
  const loadingPipe = new ObservableStatusPipe();

  it('returns state of an observable', fakeAsync(() => {
    const input: Observable<Item> = of({name: 'Item'}).pipe(delay(1000));

    const result: Observable<ObservableStatus<Item>> = loadingPipe.transform(input);

    result.subscribe(val => {
      expect(val.loading).toEqual(true);
      expect(val.value).toBeUndefined();
    });
    tick(2000);
    result.subscribe(val => {
      expect(val.loading).toEqual(false);
      expect(val.value!.name).toEqual('Item');
    });
    discardPeriodicTasks();
  }));
});

This still fails with the same errors:

Error: Expected true to equal false. (at expect(val.loading).toEqual(false))
Error: 1 periodic timer(s) still in the queue.

Can someone explain what is happening here, and how to solve this, please?

Btw, I do not want to use debounceTime, setTimeOut to solve this problem. Because they do not seem to simulate time, instead actually waits and delays time i.e. using debounceTime(1000) will actually wait for 1 sec. I do not want that in my case (I want to simulate time).

  • Using delay operator while subscribing the observable is working (without using tick).
describe('loading test', () => {
  const loadingPipe = new ObservableStatusPipe();

  it('returns state of an observable', fakeAsync(() => {
       const input: Observable<Item> = of({name: 'Item'}).pipe(delay(1000));

       const result: Observable<ObservableStatus<Item>> = loadingPipe.transform(input);

       result.subscribe(val => {
         expect(val.loading).toEqual(true);
         expect(val.value).toBeUndefined();
       });
       result.pipe(delay(2000)).subscribe(val => {
         expect(val.loading).toEqual(false);
         expect(val.value!.name).toEqual('Item');
       });
       discardPeriodicTasks();
     }));
});

Is this actually delaying/waiting for 1000 or 2000ms, or does using fakeAsync somehow let delay to simulate the time?

Thanks!


Solution

  • I think your way 1 is good but you may be facing a situation of late subscribers and multiple subscriptions. Try this:

        // Take 2 emissions
        result.pipe(take(2)).subscribe(val => {
          if (val.loading) {
            console.log('False case');
            expect(val.loading).toEqual(true);
            expect(val.value).toBeUndefined();
          } else {
            console.log('True case');
            expect(val.loading).toEqual(false);
            expect(val.value!.name).toEqual('Item');
          }
        });
        
        // Move time in a fake way. The above observable stream should hopefully emit 
        // twice and therefore there is a take(2).
        // Make sure you see both console.logs to ensure both paths were asserted.
        tick(2000);