I have a service that has 2 methods that return data from firebase realtime db
getAllProducts -> returns an observable array of products
getSingleProduct -> returns an observable single product
I am trying to create unit tests using Jest to mock firebase so I can test these 2 methods:
Test file
import {TestBed, async} from '@angular/core/testing';
import {ProductService} from './product.service';
import {AngularFireModule} from '@angular/fire';
import {environment} from 'src/environments/environment';
import {AngularFireDatabase} from '@angular/fire/database';
import {getSnapShotChanges} from 'src/app/test/helpers/AngularFireDatabase/getSnapshotChanges';
import {Product} from './product';
class angularFireDatabaseStub {
getAllProducts = () => {
return {
db: jest.fn().mockReturnThis(),
list: jest.fn().mockReturnThis(),
snapshotChanges: jest
.fn()
.mockReturnValue(getSnapShotChanges(allProductsMock, true))
};
};
getSingleProduct = () => {
return {
db: jest.fn().mockReturnThis(),
object: jest.fn().mockReturnThis(),
valueChanges: jest.fn().mockReturnValue(of(productsMock[0]))
};
};
}
describe('ProductService', () => {
let service: ProductService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AngularFireModule.initializeApp(environment.firebase)],
providers: [
{provide: AngularFireDatabase, useClass: angularFireDatabaseStub}
]
});
service = TestBed.inject(ProductService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should be able to return all products', async(() => {
const response$ = service.getAllProducts();
response$.subscribe((products: Product[]) => {
expect(products).toBeDefined();
expect(products.length).toEqual(10);
});
}));
});
allProductsMock and singleProductMock are just dummy data in the local file.
The error thrown is this.db.list is not a function.
If I change the stub to a basic constant and not a class, then the allProducts test passes, but obviously I am then stuck testing the getSingleProduct method:
const angularFireDatabaseStub = {
db: jest.fn().mockReturnThis(),
list: jest.fn().mockReturnThis(),
snapshotChanges: jest
.fn()
.mockReturnValue(getSnapShotChanges(allProductsMock, true))
};
}
So how can I make the stub more versatile and be able to also test the getSingleProduct method?
Helper
getSnapshotChanges is a helper:
import {of} from 'rxjs';
export function getSnapShotChanges(data: object, asObservable: boolean) {
const actions = [];
const dataKeys = Object.keys(data);
for (const key of dataKeys) {
actions.push({
payload: {
val() {
return data[key];
},
key
},
prevKey: null,
type: 'value'
});
}
if (asObservable) {
return of(actions);
} else {
return actions;
}
}
UPDATE
I did find one way to do both tests, but it's not very DRY having to setup the TestBed twice. Surely there must be a way to combine both stubs and inject them into the TestBed only once?
import {TestBed, async} from '@angular/core/testing';
import {ProductService} from './.service';
import {AngularFireModule} from '@angular/fire';
import {environment} from 'src/environments/environment';
import {AngularFireDatabase} from '@angular/fire/database';
import {productsMock} from '../../../../mocks/products.mock';
import {getSnapShotChanges} from 'src/app/test/helpers/AngularFireDatabase/getSnapshotChanges';
import {Product} from './product';
import {of} from 'rxjs';
const getAllProductsStub = {
db: jest.fn().mockReturnThis(),
list: jest.fn().mockReturnThis(),
snapshotChanges: jest
.fn()
.mockReturnValue(getSnapShotChanges(productsMock, true))
};
const getSingleProductStub = {
db: jest.fn().mockReturnThis(),
object: jest.fn().mockReturnThis(),
valueChanges: jest.fn().mockReturnValue(of(productsMock[0]))
};
describe('getAllProducts', () => {
let service: ProductService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AngularFireModule.initializeApp(environment.firebase)],
providers: [{provide: AngularFireDatabase, useValue: getAllProductsStub}]
}).compileComponents();
service = TestBed.inject(ProductService);
});
it('should be able to return all products', async(() => {
const response$ = service.getAllProducts();
response$.subscribe((products: Product[]) => {
expect(products).toBeDefined();
expect(products.length).toEqual(10);
});
}));
});
describe('getSingleProduct', () => {
let service: ProductService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AngularFireModule.initializeApp(environment.firebase)],
providers: [{provide: AngularFireDatabase, useValue: getSingleProductStub}]
}).compileComponents();
service = TestBed.inject(ProductService);
});
it('should be able to return a single product using the firebase id', async(() => {
const response$ = service.getSingleProduct('-MA_EHxxDCT4DIE4y3tW');
response$.subscribe((product: Product) => {
expect(product).toBeDefined();
expect(product.id).toEqual('-MA_EHxxDCT4DIE4y3tW');
});
}));
});