用于 Angular 服务测试的 Jasmine 可重用 FireStore 模拟
Jasmine reusable FireStore mock for Angular service testing
我正在尝试制作一个可重复使用的 firestore 模拟来测试多个 angular 服务。
我的服务是这样的:
@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);
})
);
}
}
我已经能够制作一个简单的函数来模拟 firestore 并测试 firestore 集合和快照,但我无法即时更改 returned 数据,所以我需要重写它每次。
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)) });
还有我的测试:
describe('DataSheetService', () => {
const payload = [{ name: 'lamps' }, { name: 'mirrors' }];
let service: DataSheetService;
let angularFirestore: AngularFirestore;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
DataSheetService,
{ provide: AngularFirestore, useValue: angularFireDatabaseStub(payload) },
{ provide: AuthService, useClass: AuthServiceMock },
],
});
service = TestBed.inject(DataSheetService);
angularFirestore = TestBed.inject(AngularFirestore);
});
it('should be created', () => {
expect(service).toBeTruthy();
expect(angularFirestore.collection).toHaveBeenCalled();
});
it('should return list of data sheets types', async () => {
const types$ = service.getDataSheetsTypes();
types$.subscribe((types) => {
expect(types.length).toBe(2);
expect(Object.keys(types[1])).toContain('id');
expect(types[1]).toEqual(jasmine.objectContaining({ name: 'mirrors' }));
});
});
});
使用 angularFireDatabaseStub 函数,所有测试都通过了,但我想要实现的是第三种情况,其中来自 service.getDataSheetsTypes 方法的 returned 数据发生变化,由于此数据在 angularFireDatabaseStub 的调用中被硬编码,我正在尝试采用不同的方法。
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),
};
}
}
有了这个 class 我可以设置一个初始的 return 数据,理论上,可以通过调用 setReturnData 方法设置一个新数据,但是当我导入 class 进行测试,第一个失败,即使我在第二种情况之前调用 setReturnData 方法(此方法实际上记录了更新的 return 数据),数据在测试中也没有更改。
这是我对 FireStoreMock class
的更新测试
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' }));
});
});
});
有什么方法可以实现这种行为...?
我只会使用 jasmine.createSpyObj
而不是明确地模拟它。类似于 this.
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'); // move spy here because the service gets
service = TestBed.inject(DataSheetService); // instantiated on the next line
angularFirestore = TestBed.inject(AngularFireStore) as jasmine.SpyObj<FireStoreMock>;// and if you spy after this line
}); // it will be too late.
it('should be created', () => { // moving the spy should make this test
expect(service).toBeTruthy(); // pass
expect(angularFirestore.collection).toHaveBeenCalled(); // <-- this one fails
});
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
});
// this test may need some more work but you get the idea now
// looking at the line above, you can mock the next function call of what you're
// mocking and then call your service and get the results you expect.
const types$ = service.getDataSheetsTypes();
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' }));
});
});
});
我正在尝试制作一个可重复使用的 firestore 模拟来测试多个 angular 服务。 我的服务是这样的:
@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);
})
);
}
}
我已经能够制作一个简单的函数来模拟 firestore 并测试 firestore 集合和快照,但我无法即时更改 returned 数据,所以我需要重写它每次。
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)) });
还有我的测试:
describe('DataSheetService', () => {
const payload = [{ name: 'lamps' }, { name: 'mirrors' }];
let service: DataSheetService;
let angularFirestore: AngularFirestore;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
DataSheetService,
{ provide: AngularFirestore, useValue: angularFireDatabaseStub(payload) },
{ provide: AuthService, useClass: AuthServiceMock },
],
});
service = TestBed.inject(DataSheetService);
angularFirestore = TestBed.inject(AngularFirestore);
});
it('should be created', () => {
expect(service).toBeTruthy();
expect(angularFirestore.collection).toHaveBeenCalled();
});
it('should return list of data sheets types', async () => {
const types$ = service.getDataSheetsTypes();
types$.subscribe((types) => {
expect(types.length).toBe(2);
expect(Object.keys(types[1])).toContain('id');
expect(types[1]).toEqual(jasmine.objectContaining({ name: 'mirrors' }));
});
});
});
使用 angularFireDatabaseStub 函数,所有测试都通过了,但我想要实现的是第三种情况,其中来自 service.getDataSheetsTypes 方法的 returned 数据发生变化,由于此数据在 angularFireDatabaseStub 的调用中被硬编码,我正在尝试采用不同的方法。
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),
};
}
}
有了这个 class 我可以设置一个初始的 return 数据,理论上,可以通过调用 setReturnData 方法设置一个新数据,但是当我导入 class 进行测试,第一个失败,即使我在第二种情况之前调用 setReturnData 方法(此方法实际上记录了更新的 return 数据),数据在测试中也没有更改。 这是我对 FireStoreMock class
的更新测试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' }));
});
});
});
有什么方法可以实现这种行为...?
我只会使用 jasmine.createSpyObj
而不是明确地模拟它。类似于 this.
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'); // move spy here because the service gets
service = TestBed.inject(DataSheetService); // instantiated on the next line
angularFirestore = TestBed.inject(AngularFireStore) as jasmine.SpyObj<FireStoreMock>;// and if you spy after this line
}); // it will be too late.
it('should be created', () => { // moving the spy should make this test
expect(service).toBeTruthy(); // pass
expect(angularFirestore.collection).toHaveBeenCalled(); // <-- this one fails
});
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
});
// this test may need some more work but you get the idea now
// looking at the line above, you can mock the next function call of what you're
// mocking and then call your service and get the results you expect.
const types$ = service.getDataSheetsTypes();
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' }));
});
});
});