Angular单元测试:正确Mock AngularFireStore集合get调用的问题
编辑说明
今日取得了一些进展,但同一流程中遇到新错误,以下是更新后的问题。
我需要Mock AngularFireStore的复杂调用,测试调用服务的importData函数时一直报错。最终的get调用应返回供校验的数组,但Mock配置不正确,导致测试失败。
目前Mock版本2已接近可用,解决了之前的函数相关错误,但现在第一个IF语句检查返回数组长度时失败,返回值为undefined,触发错误。
有意思的是:
- 若把Mock里的最终返回改成
return Promise.resolve()(不传数组),错误变为:Error: Uncaught (in promise): TypeError: Cannot read properties of undefined (reading 'docs') - 传入数组时,错误变为读取
undefined的length属性
我怀疑和fakeasync的处理有关,试过几种写法,当前测试用的是fakeasync。另外,如果改成普通的async加/不加await,测试显示“通过”但控制台仍抛出错误:
TypeError: Error: Uncaught (in promise): TypeError: Cannot read properties of undefined (reading 'length')
TypeError: Cannot read properties of undefined (reading 'length')
service.ts
importData(Id: number) { this.afs.collection<data>('users/' + this.authService.liveUid) .ref.where('id', '==', Id) .get() .then( a => { if (a.docs.length == 0) { if (validID(Id, 8, 9)) { this.http.get<DataImport>('https://url' + Id).subscribe( //do stuff ) } else { this.urlError = true this.spinnerImported = true } } else { this.spinnerImported = true this.sameImportName = a.docs[0].get('name') } }) }
注:原代码存在语法错误,已修正'users/' + this.authService.liveUid + .ref中的多余加号
service.spec.ts
describe('ImportService', () => { let service: ImportService; beforeEach(() => { TestBed.configureTestingModule( { providers: [ { provide: HttpClient, useValue: httpClientMock }, { provide: AngularFirestore, useValue: AngularFirestoreMock }, { provide: AuthService, useValue: AuthServiceMock }, ] }); service = TestBed.inject(ImportService); }); it('should be created', fakeasync (() => { service.importData(32198712) tick () })); });
AngularFirestoreMock(版本1)
export const AngularFirestoreMock = jasmine.createSpyObj('AngularFirestoreMock', { 'doc': { set: function () { return Promise.resolve() } }, 'collection':{ get: function () { return Promise.resolve() } } })
AngularFirestoreMock(版本2)
export const AngularFirestoreMock = jasmine.createSpyObj('AngularFirestoreMock', { 'doc': { set: function () { return Promise.resolve() } }, 'collection': { 'ref': { where: () => { return { get: () => { return Promise.resolve([{ name: 'test'}]) } } } } } })
解决方法
问题核心是Mock返回的结构和AngularFirestore实际返回的QuerySnapshot不匹配:实际get()返回的是包含docs数组的对象,而非直接返回数组。
修正后的AngularFirestoreMock
让get()返回模拟的QuerySnapshot对象,包含docs属性,每个doc还要实现get()方法:
export const AngularFirestoreMock = jasmine.createSpyObj('AngularFirestoreMock', { 'doc': { set: () => Promise.resolve() }, 'collection': () => ({ ref: { where: () => ({ get: () => Promise.resolve({ // 模拟QuerySnapshot的docs数组 docs: [ { get: (field: string) => { if (field === 'name') return 'test'; return null; } } ] }) }) } }) });
若要模拟空结果(触发a.docs.length === 0分支),只需把docs设为空数组:
get: () => Promise.resolve({ docs: [] })
测试优化建议
- 使用
fakeAsync时,用flush()替代tick(),确保所有异步操作完成,避免控制台残留报错:
it('should handle existing data', fakeAsync(() => { service.importData(32198712); flush(); // 触发所有异步操作完成 expect(service.sameImportName).toBe('test'); expect(service.spinnerImported).toBe(true); }));
- 若用
async/await,建议修改importData让它返回Promise,方便测试等待:
// 修改service.ts的importData,返回Promise importData(Id: number): Promise<void> { return this.afs.collection<data>('users/' + this.authService.liveUid) .ref.where('id', '==', Id) .get() .then( a => { // ...原有逻辑 }) } // 测试代码 it('should handle existing data', async () => { await service.importData(32198712); expect(service.sameImportName).toBe('test'); expect(service.spinnerImported).toBe(true); });
内容的提问来源于stack exchange,提问作者Rusty Shackleford

