AngularFireDatabase, Jest and Unit Testing, how to create a reusable class stub?

Viewed 291

I have a test file that is testing a service that returns data from AngularFireDatabase:

import {TestBed, async} from '@angular/core/testing';
import {ProductService} from './product.service';
import {AngularFireDatabase} from '@angular/fire/database';
import {productsMock} from '../../../../mocks/products.mock';
import {Product} from 'shared';
import {Observable} from 'rxjs';
import {getSnapShotChanges} from 'src/app/test/helpers/AngularFireDatabase/getSnapshotChanges';

let list: Product[];
let key: string = '';

const afDatabaseStub = {
  db: jest.fn().mockReturnThis(),
  list: jest.fn(() => ({
    snapshotChanges: jest.fn().mockReturnValue(getSnapShotChanges(list, true)),
    valueChanges: jest.fn(
      () => new Observable(sub => sub.next(Object.values(list)))
    )
  })),
  object: jest.fn(() => ({
    valueChanges: jest.fn(() => new Observable(sub => sub.next({id: key})))
  }))
};

describe('ProductService', () => {
  let service: ProductService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{provide: AngularFireDatabase, useValue: afDatabaseStub}]
    });
    service = TestBed.inject(ProductService);
  });

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

  describe('getAllProducts', () => {
    it('should be able to return all products', async(() => {
      list = productsMock;

      service.getAllProducts().subscribe((products: Product[]) => {
        expect(products?.length).toEqual(10);
      });
    }));
  });

  it('should be able to return a single product using the firebase id', async(() => {
    key = '-MA_EHxxDCT4DIE4y3tW'
    const response$ = service.getProductById(key);
    response$.subscribe((giveawayProduct: GiveawayProduct) => {
      expect(giveawayProduct).toBeDefined();
      expect(giveawayProduct.id).toEqual(key);
    });
  }));

});

The problem I am facing is that I now want to test another service that also uses AngularFireDatabase.

So how can I make this stub more general purpose and put it into a shared helper file that I can use in different specs?

For example, I know you can do useClass instead of useValue:

providers: [{provide: AngularFireDatabase, useClass: afDatabaseStub}]

If it was a class then list and key could be class properties that I could set before running the tests.

But when I try that, I get errors like this:

db.list.object is not a function

db.list(...).snapshotchanges is not a function

2 Answers

I think the best way to do this is to create an abstraction layer between AngularFire and your components. Something like this:

interface IProductService {
    getProducts(): Observable<product>;
    getProduct(id: string): Observable<product>;
    //And all your other methods.
}

Now create your product service which implements the interface:

ProductService implements IProductService {
    constructor(angularFire: AngularFire){}

    getProducts(): Observable<product>{
        return this.angularFire....
    }
    //And all your other methods.
}

Now for your tests you can create a veery simple mock instance:

MockProductService implements IProductService {
    constructor(){}

    getProducts(): Observable<product>{
        return of([new Product("One"), new Product("Two")])
    }
    //And all your other methods.
}

You can make your mock as easy or as complex as you need it.

All you need you already have. Next thing you need to do is to extract mock object into some .ts file and export it.

I usually create test-helpers folder and providers.ts in it. There I declare some functions that provide me most common providers mocks.

export function getAngularFireMock() {
  const afDatabaseStub = {
    db: jest.fn().mockReturnThis(),
    list: jest.fn(() => ({
      snapshotChanges: jest.fn().mockReturnValue(getSnapShotChanges(list, true)),
      valueChanges: jest.fn(
        () => new Observable(sub => sub.next(Object.values(list)))
      )
    })),
    object: jest.fn(() => ({
      valueChanges: jest.fn(() => new Observable(sub => sub.next({id: key})))
    }))
  };

  return {provide: AngularFireDatabase, useValue: afDatabaseStub};
}

Then you simply call getAngularFireMock() inside providers array and that's it.

Then if you need change values for other tests you simply mock them using Jest's API.

Related