I am currently working with a pipe that assists in returning the state of an observable.
import {Pipe, PipeTransform} from '@angular/core';
import {Observable, of} from 'rxjs';
import {catchError, map, startWith} from 'rxjs/operators';
/** Defines the status of an Observable. */
export interface ObservableStatus<T> {
loading?: boolean;
value?: T;
error?: boolean;
}
/** Retrieves 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 am looking to create unit tests for this feature using Jasmine. I attempted to use
fakeAsync, delay, tick, flush, discardPeriodicTasks
but encountered some difficulties.
I have experimented with different approaches:
- Approach 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 is failing with the following errors:
Error: Expected true to equal false. (at expect(val.loading).toEqual(false))
Error: 1 periodic timer(s) still in the queue.
- Approach 2: I came across the usage of flush online to clear 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 helps in resolving the
Error: 1 periodic timer(s) still in the queue.
issue. However, the test case continues to fail with:
Error: Expected true to equal false.
TypeError: Cannot read properties of undefined (reading 'name')
Is it possible that the tick
function is not properly simulating time on the input
observable?
- I conducted the same simulation directly on the
input
observable:
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 unsure why flush() is not functioning correctly in this scenario.
- Approach 3: I tried utilizing the aforementioned discardPeriodicTasks trick to address my initial issue.
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 results in the same errors:
Error: Expected true to equal false. (at expect(val.loading).toEqual(false))
Error: 1 periodic timer(s) still in the queue.
If anyone can shed light on what is happening here and how to resolve it, I would greatly appreciate it.
Additionally, I prefer not to resort to using debounceTime
, setTimeOut
as solutions for this problem. These methods do not seem to simulate time effectively, rather they actually wait and delay time i.e. debounceTime(1000) will wait for 1 second. This is not suitable for my requirements (I want to simulate time).
- Using
delay
operator while subscribing to the observable seems to be effective (without employingtick
).
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 truly delaying/waiting for 1000 or 2000ms, or does fakeAsync allow delay to accurately simulate time?
Thank you!