Angular单元测试:如何Mock Firestore的arrayUnion与arrayRemove
问题场景
我有两个处理数组增删的相似函数,在单元测试里调用Firestore的arrayUnion和arrayRemove时触发报错,错误提示程序跳过Mock提供者、尝试调用真实服务,但不清楚需要Mock哪些内容才能拦截这两个函数的调用。
服务中两个函数的导入路径不同:
import { arrayRemove, arrayUnion } from '@angular/fire/firestore'; import { AngularFirestore } from '@angular/fire/compat/firestore';
错误信息
Error: Either AngularFireModule has not been provided in your AppModule (this can be done manually or implictly using provideFirebaseApp) or you're calling an AngularFire method outside of an NgModule (which is not supported).
相关代码
service.ts
createCharacterPartyLink(characters: any[], name: string, partyId: string) { const characterPartyCollectionRef = this.afs.collection<character>('users/' + this.uid + '/Characters/') characters.forEach((char: any) => { characterPartyCollectionRef.doc(char.characterid).update( { memberOf: arrayUnion({ partyId: partyId, partyName: name }) } )})}
service.spec.ts
beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: AngularFirestore, useValue: AngularFirestoreMock }, { provide: AngularFireAuth, useValue: AngularFireAuthMock } ] }); service = TestBed.inject(PartyService); }); it('createCharacterPartyLink Work as Expected', () => { const updateObj = { update: function () { return { arrayUnion: ()=> { }} }, }; const updateSpy = spyOn(updateObj, 'update'); AngularFirestoreMock.collection.and.returnValue({ doc: () => { return updateObj } } ) service.createCharacterPartyLink(mockPartyMembers, 'Test Party', '1234') expect(updateSpy).toHaveBeenCalled() });
问题原因
arrayUnion和arrayRemove是独立的工具函数,并非AngularFirestore类的方法,它们直接从@angular/fire/firestore导入。调用这些函数时,会尝试访问Firebase底层实例,而测试环境未初始化AngularFireModule,因此触发报错。之前只Mock了AngularFirestore类,没有处理这两个独立函数,导致测试时调用了真实函数。
解决方案
1. Mock独立的arrayUnion/arrayRemove函数
在测试文件中直接Mock这两个函数,避免它们调用真实服务:
Jasmine示例
// 先导入目标函数 import { arrayUnion, arrayRemove } from '@angular/fire/firestore'; beforeEach(() => { // Mock arrayUnion,返回一个可识别的标识值 spyOn(window, 'arrayUnion').and.returnValue('MOCKED_ARRAY_UNION'); // 同理Mock arrayRemove spyOn(window, 'arrayRemove').and.returnValue('MOCKED_ARRAY_REMOVE'); TestBed.configureTestingModule({ providers: [ { provide: AngularFirestore, useValue: AngularFirestoreMock }, { provide: AngularFireAuth, useValue: AngularFireAuthMock } ] }); service = TestBed.inject(PartyService); });
Jest示例
// 在文件顶部Mock模块 jest.mock('@angular/fire/firestore', () => ({ ...jest.requireActual('@angular/fire/firestore'), arrayUnion: jest.fn(() => 'MOCKED_ARRAY_UNION'), arrayRemove: jest.fn(() => 'MOCKED_ARRAY_REMOVE') })); beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: AngularFirestore, useValue: AngularFirestoreMock }, { provide: AngularFireAuth, useValue: AngularFireAuthMock } ] }); service = TestBed.inject(PartyService); });
2. 完善测试逻辑,验证调用正确性
修正测试代码,不仅验证update被调用,还要验证arrayUnion的调用参数和时机:
it('createCharacterPartyLink 执行符合预期', () => { // 创建层级Mock对象 const mockUpdate = jasmine.createSpy('update'); const mockDoc = jasmine.createSpy('doc').and.returnValue({ update: mockUpdate }); const mockCollection = jasmine.createSpy('collection').and.returnValue({ doc: mockDoc }); AngularFirestoreMock.collection = mockCollection; // 调用测试方法 service.createCharacterPartyLink(mockPartyMembers, 'Test Party', '1234'); // 验证collection路径正确 expect(mockCollection).toHaveBeenCalledWith('users/' + service.uid + '/Characters/'); // 验证每个角色都触发了doc和update调用 expect(mockDoc).toHaveBeenCalledTimes(mockPartyMembers.length); expect(mockUpdate).toHaveBeenCalledTimes(mockPartyMembers.length); // 验证arrayUnion被传入正确参数 expect(arrayUnion).toHaveBeenCalledWith({ partyId: '1234', partyName: 'Test Party' }); });
3. 补充:确保uid在测试环境有值
如果服务依赖this.uid,需要在AngularFireAuthMock中设置该值:
const AngularFireAuthMock = { currentUser: Promise.resolve({ uid: 'test-user-id' }) // 或根据服务获取uid的方式调整,比如返回同步值 };
内容的提问来源于stack exchange,提问作者Rusty Shackleford

