Experiment with Observable in fakeAsync, applying a delay and tick functions through Jasmine testing framework

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 employing 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 truly delaying/waiting for 1000 or 2000ms, or does fakeAsync allow delay to accurately simulate time?

Thank you!

Answer №1

Consider implementing this alternative approach if you are dealing with a scenario involving delayed subscribers and multiple subscriptions:

    // Grabbing 2 emissions
    result.pipe(take(2)).subscribe(val => {
      if (val.loading) {
        console.log('Executing false case');
        expect(val.loading).toEqual(true);
        expect(val.value).toBeUndefined();
      } else {
        console.log('Executing true case');
        expect(val.loading).toEqual(false);
        expect(val.value!.name).toEqual('Item');
      }
    });
    
    // Simulate time passing in an artificial manner. The observable stream mentioned above should emit twice due to the take(2).
    // Ensure that both console.logs are displayed, validating that both scenarios were tested.
    tick(2000);

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

AngularFire UPDATE -> APPLY CHANGES

I can't seem to figure this out. I'm wondering how to UPDATE a document that is returned in the WHERE clause using AngularFire: constructor(private db: AngularFirestore) { } var path = this.db.collection('users').doc('type') ...

Attempting a second filter of the table using the dropdown results in no data being returned

I've developed a CRUD app using Angular 7, and I'm facing an issue. When I select a dropdown item for the first time, it shows the desired table data. However, on selecting another item for the second time, it returns nothing. Below is my compone ...

Is there a way to transform individual data into a collective group dataset?

Here is the input data provided: data = [ { name: "John Cena", groupName: "WWE" }, { name: "Nandini", groupName: null }, { name: "Rock", groupName: "WWE" }, { name: "Vinay", groupName: null }, { name: "Rey Mesterio", groupName: "WWE" ...

LeafletJS tiles are only loaded once the map is being actively moved

Currently, I am facing an issue while trying to load a simple leaflet map in my Ionic 2 application. The problem is that not all tiles are loading correctly until the map is moved. this.map = new L.Map('mainmap', { zoomControl: false, ...

Always deemed non-assignable but still recognized as a universal type?

I'm curious about why the never type is allowed as input in generic's extended types. For example: type Pluralize<A extends string> = `${A}s` type Working = Pluralize<'language'> // 'languages' -> Works as e ...

The 'export '__platform_browser_private__' could not be located within the '@angular/platform-browser' module

I have encountered an issue while developing an angular application. Upon running ng serve, I am receiving the following error in ERROR in ./node_modules/@angular/http/src/backends/xhr_backend.js 204:40-68: "export 'platform_browser_private' w ...

Could a variable (not an element) be defined and controlled within an Angular 2 template?

For instance, envision a scenario where I have a series of input fields and I wish to assign the 'tab' attribute to them sequentially as we move down the page. Rather than hard-coding the tab numbers, I prefer to utilize a method that automatical ...

The PrimeNG table fails to refresh upon initial modification

I'm working with a prime-ng table of shops, where I have the ability to remove and add shops to a list. The scenario: Whenever a shop is added, the ChildComponent emits an event to the ParentComponent, which then adds the shop to the list and updates ...

Configuring eslint-import-resolver-typescript in a multi-package repository

I'm currently working on implementing path-mapping within a monorepo structure. Despite having existing eslint-plugin-import rules in place, I am encountering an error stating "Unable to resolve path to module" for all mapped imports. app/ ├─ pack ...

Combine the object with TypeScript

Within my Angular application, the data is structured as follows: forEachArrayOne = [ { id: 1, name: "userOne" }, { id: 2, name: "userTwo" }, { id: 3, name: "userThree" } ] forEachArrayTwo = [ { id: 1, name: "userFour" }, { id: ...

Best Practices for Integrating Angular with Your Custom JavaScript Library

Imagine needing to create a TypeScript function that can be utilized across various components, services, or modules. For example, let's say you want an alert wrapper like this: my_alert(msg); // function my_alert(msg) { alert(msg); } You might hav ...

In my Angular project, I'm looking to show the date and time based on the specific timezone

I am currently showcasing the IST time zone, but I would like to adjust it based on the user's location. Here is an example code snippet from app.component.html: <td>{{scan.createdOn + 'Z' | date :'medium'}}</td> ...

What is the best way to retrieve a property value from an object using the .find() method?

I've encountered a problem with the following code snippet in my function: let packName: string = respPack.find(a => {a.id == 'name_input'}).answer.replace(/ /,'_'); My goal is to locate an object by matching its id and retrie ...

Challenges with variable scopes and passing variables in Ionic 2 (Typescript)

In my Ionic 2 TypeScript file, I am facing an issue with setting the value of a variable from another method. When I close the modal, I get undefined as the value. I'm encountering difficulty in setting the value for coord. export class RegisterMapP ...

Using TypeScript, pass an image as a prop in a Styled Component

I am facing an issue with the code below that is supposed to display the "NoBillsLaptopPNG.src" image on the screen, but for some reason, the image is not showing up. The images are being imported correctly, so I'm unsure why the image is not appeari ...

How can I use Typescript to define a function that accepts a particular string as an argument and returns another specific string?

I've been working on this code snippet: const Locales = { en_gb: 'en-gb', en_us: 'en-us', } as const type ApiLocales = typeof Locales[keyof typeof Locales] type DatabaseLocales = keyof typeof Locales function databaseLanguage ...

What is the best way to transform the data stored in Observable<any> into a string using typescript?

Hey there, I'm just starting out with Angular and TypeScript. I want to get the value of an Observable as a string. How can this be achieved? The BmxComponent file export class BmxComponent { asyncString = this.httpService.getDataBmx(); curr ...

Issue with Angular 2: MaterializeCSS module not loading correctly when using routing

I am currently incorporating MaterializeCSS into my Angular project. It appears that materialisecc.js and/or jquery.js are loaded with routing, causing the need to reload each page of the app for it to function properly. This issue is affecting the overall ...

What's the simplest method for updating a single value within a nested JSON object using TypeScript?

Using TypeScript version ^3.5.3 Consider the following JSON data: const config = { id: 1, title: "A good day", body: "Very detailed stories" publishedAt: "2021-01-20 12:21:12" } To update the title using spread synta ...

Using Typescript to Integrate node-gtf JavaScript Library into an Express Application

Would like to utilize a Typescript Express Server for integrating GTFS data with the help of the GTFS library (https://github.com/BlinkTagInc/node-gtfs) current version is ("gtfs": "^3.0.4") This is how I am importing the library imp ...