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?