FireStore mock for Angular service testing that can be reused by Jasmine

I am in the process of creating a reusable Firestore mock for testing various Angular services. The structure of my services is as follows:

@Injectable({
  providedIn: 'root',
})
export class DataSheetService {
  dataSheetTypesDbRef: AngularFirestoreCollection<DataSheetType>;

  constructor(private db: AngularFirestore) {
    this.dataSheetTypesDbRef = this.db.collection<DataSheetType>(DBNAMES.dataSheetsTypes);
  }

  getDataSheetsTypes(): Observable<DataSheetType[]> {
    return this.dataSheetTypesDbRef.snapshotChanges().pipe(
      map((actions) => {
        return actions.map((a) => {
          const data = a.payload.doc.data();
          const id = a.payload.doc.id;
          return { id, ...data };
        });
      })
    );
  }

  saveDataSheetType(newType): Observable<DataSheetType> {
    return from(
      this.dataSheetTypesDbRef
        .add(typeToSave)
        .then((docRef) => {
          return { id: docRef.id, ...typeToSave };
        })
        .catch((e) => {
          throw new Error(e);
        })
    );
  }
}

I have managed to create a basic function to mock Firestore and test the collection and snapshot functions. However, I am facing an issue where I cannot dynamically change the returned data, so I have to rewrite it every time.

const formatData = (data) => {
  const dataToReturn = data?.map((data) => {
    const { id, ...docData } = data;
    return {
      payload: {
        doc: {
          data: () => docData,
          id: id || Math.random().toString(16).substring(2),
        },
      },
    };
  });
  return dataToReturn;
};

const collectionStub = (data) => ({
  snapshotChanges: () => of(formatData(data)),
});
export const angularFireDatabaseStub = (data) => ({ collection: jasmine.createSpy('collection').and.returnValue(collectionStub(data)) });

Here is my current approach to tackling this issue:

export class FireStoreMock {
  returnData: any[];
  constructor(data) {
    this.returnData = data;
  }

  setReturnData(data: any[]) {
    this.returnData = data;
    console.log(this.returnData);
  }

  formatData(data) {
    const dataToReturn = data?.map((data) => {
      const { id, ...docData } = data;
      return {
        payload: {
          doc: {
            data: () => docData,
            id: id || Math.random().toString(16).substring(2),
          },
        },
      };
    });
    return dataToReturn;
  }

  snapshotChanges() {
    return of(this.formatData(this.returnData)).pipe(
      tap((res) => console.log("snapshot res", res))
    );
  }

  collection() {
    console.log("collection called");
    const _this = this;
    return {
      snapshotChanges: _this.snapshotChanges.bind(this),
    };
  }
}

I have also updated my test code accordingly:

describe('DataSheetService', () => {
  const payload = [{ name: 'lamps' }, { name: 'mirrors' }];
  const payloadAlt = [{ name: 'lamps' }, { name: 'tables' }];
  let service: DataSheetService;
  let angularFirestore: FireStoreMock;
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: AngularFirestore, useValue: new FireStoreMock(payload) },
      ],
    });
    service = TestBed.inject(DataSheetService);
    angularFirestore = new FireStoreMock(payload);
    spyOn(angularFirestore, 'collection')
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
    expect(angularFirestore.collection).toHaveBeenCalled(); // <-- this one fails
  });

  it('should return list of data sheets types', async () => {
    const types$ = service.getDataSheetsTypes();
    angularFirestore.setReturnData(payloadAlt)
    types$.subscribe((types) => {
      console.log('types', types); // <- this logs [{ name: 'lamps' }, { name: 'mirrors' }]
      expect(types.length).toBe(2);
      expect(Object.keys(types[1])).toContain('id');
      expect(types[1]).toEqual(jasmine.objectContaining({ name: 'tables' }));
    });
  });
});

I am still facing some difficulties in achieving the desired behavior. Is there any way to solve this issue?

Answer №1

If I were to tackle this, I would personally opt for using jasmine.createSpyObj rather than explicitly mocking it. You can refer to a similar approach outlined in this resource.


describe('DataSheetService', () => {
  const payload = [{ name: 'lamps' }, { name: 'mirrors' }];
  const payloadAlt = [{ name: 'lamps' }, { name: 'tables' }];
  let service: DataSheetService;
  let angularFirestore: jasmine.SpyObj<FireStoreMock>;
  beforeEach(() => {
    const spy = jasmine.createSpyObj('AngularFirestore', ['collection']);
    TestBed.configureTestingModule({
      providers: [
        { provide: AngularFirestore, useValue: spy },
      ],
    });
    spyOn(angularFirestore, 'collection'); // Note: moving the spy here is essential as the service gets instantiated in the next line
    service = TestBed.inject(DataSheetService);
    angularFirestore = TestBed.inject(AngularFireStore) as jasmine.SpyObj<FireStoreMock>;// ensure that you set up the spy before this point
  });

  it('should be created', () => {                 
    expect(service).toBeTruthy();                
    expect(angularFirestore.collection).toHaveBeenCalled(); 
  });

  it('should return list of data sheets types', async () => {
    // mock the next call to collection to return this object
    angularFirestore.collection.and.returnValue({
      snapshotChanges: () => of(/* */), // your mock data inside of of
    });
  
    const types$ = service.getDataSheetsTypes();
    types$.subscribe((types) => {
      console.log('types', types); // logs [{ name: 'lamps' }, { name: 'mirrors' }]
      expect(types.length).toBe(2);
      expect(Object.keys(types[1])).toContain('id');
      expect(types[1]).toEqual(jasmine.objectContaining({ name: 'tables' }));
    });
  });
});

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

The assets path is the directory within the installed package that houses the main application files following the completion of a

I have a Vue.js UI component that is internally built using webpack. This reusable UI component library references its images as shown below: <img src="./assets/logo.png"/> <img src="./assets/edit-icon.svg"/>   <i ...

Can Firebase Analytics be configured through an Express server?

I'm working on an Express-based application and I'm interested in integrating Firebase Analytics to track events. However, after exploring the documentation, it seems that incorporating this feature with Node.js may not be possible. Though there ...

Filtering Angular routing history elements

If I have an application that relies on Router for navigation, is there a way to eliminate certain router history elements from the history? For example, is it possible to programmatically filter out all URLs containing 'xyz' like this: // Exampl ...

Tips for ensuring the update operation is secure in this given situation

I developed an app that provides gift recommendations to users based on their responses to a few questions. Whenever a user likes or dislikes a gift, I need to update the 'liked' field in the database with an update request. Users do not have to ...

Next.js API is throwing a TypeError because req.formData is not a recognized function

Below is the code snippet for the Next.js route I am working on: import { NextRequest, NextResponse } from 'next/server'; export const config = { runtime: 'edge', }; export default async function POST(req: NextRequest): Promise< ...

Altering the parent component's output depending on a boolean value established in the child component within Angular

Recently I began learning Angular and find myself in need of some assistance when it comes to managing a specific situation with Angular 16. In our project, we have two different versions of the site header represented by two components - one as the defaul ...

Issue with TypeScript retrieving value from an array

Within my component.ts class, I have defined an interface called Country: export interface Country{ id: String; name: String; checked: false; } const country: Country[] = [ { id: 'India', name: 'India', checked: false}, { ...

Angular 2/NPM: setting up a new directory with all the necessary files for running the application

I'm feeling a bit frustrated, but I'm giving it a shot anyway. Our goal is to create a TypeScript Angular 2 hello world app that we can use as the front end for a Spring app. Currently, we're using the Angular 2 quickstart as our foundation ...

Displaying error 400 information on the frontend using Angular

I encountered an error message when using a search form and I need to show the specifics of this error to the user on the Angular frontend. The JSON response code that appears in the Devtools network tab is as follows: { "schemas":[&qu ...

Navigating with Leaflet.PolylineMeasure in Angular app

The challenge I'm facing involves integrating the Leaflet.PolylineMeasure plugin into my Angular application, which is written in JavaScript. After attempting to npm install the library from https://www.npmjs.com/package/leaflet.polylinemeasure, I enc ...

Setting up Weblogic application server for an Angular application: A Step-by-Step Guide

I am facing a deployment issue with my Angular (6.1) application that is packaged in a WAR and EAR file for deployment on a Weblogic server (12c). According to the guidelines provided here, all requests should be directed to the application's index.ht ...

Using *ngFor to show subcategories within parent categories based on a condition

Trying to implement a feature to display subcategories under categories using JSON response in an angular 2 application. Utilizing ngIf to filter subcategories based on the parent_id field in the JSON string. JSON Data: [ { "id": "15", "parent_ ...

Executing unit tests in Angular - launch Chrome upon successful build completion (which may take a while)

There are instances where the Angular app takes longer than the default 2-minute timeout for Chrome started by Karma to capture the content. Is there a method to compel Karma to launch Chrome after the build is completed? In my package.json: { "depende ...

Angular: rendering a dynamic index.html

Currently, our team is integrating AngularJS and Asp.Net Web API (not Core) and we are looking to transition to Angular in hybrid mode. Our server delivers index.hbs (handlebars) which contains dynamic files based on specific parameters. <link rel= ...

Adjust the font size in Chart.js for improved resolution across all types of monitors (i.e. creating charts that are not specific

Everything is looking great on my chart with the labels perfectly displayed at the defined font size. However, I am facing an issue when viewing the chart in higher resolutions as the font sizes appear small. Is there a way to dynamically adjust all size-r ...

The base class is invoking a function from its child class

There are two classes, a base class and a derived one, each with an init function. When constructing the derived class, it should: Call its base constructor which: 1.1. Calls its init function Call its own (derived) init function. The issue is that ...

Eslint is back and it's cracking down on unused variables with no

I've configured eslint to alert me about unused variables rules: { '@typescript-eslint/no-unused-vars': ['error', { args: 'none' }], } Presently, I have a TypeScript class structured like this: import { User } from &ap ...

Angular 6 - Struggling to translate "Invalid Date" to a valid date using the "DatePipe" filter

Greetings, I am encountering an issue related to converting dates into a specific format that I desire. Currently, the REST API is sending dates in Milliseconds format, which I need to convert to a date format like yyyy-MM-dd for my date-picker component ...

What steps are required to utilize NgbSortableHeader for sorting a bootstrap table through programming?

I have a bootstrap HTML table (operated by ng-bootstrap for Angular and utilized NgbdSortableHeader to arrange table columns by clicking on the column). When I click on an element, it sorts the column in ascending, descending, or ''(none) order. ...

Transferring Files from Bower to Library Directory in ASP.Net Core Web Application

Exploring ASP.Net Core + NPM for the first time, I have been trying out different online tutorials. However, most of them don't seem to work completely as expected, including the current one that I am working on. I'm facing an issue where Bower ...