Angular Nx单仓Jest测试导入Firebase库报fetch未定义错误求助
Angular + Nx单仓Jest测试中Firestore
fetch is not defined 问题排查 问题背景
我维护一个Angular + Nx单仓项目,其中一个类库封装了访问Firestore的FirestoreService。生产环境下应用运行完全正常,但只要组件测试中间接引用了该服务,Jest就会抛出fetch is not defined的错误——即使没有在测试中显式注册该服务也会触发这个问题。类库内部的Jest测试一切正常,只有应用侧的组件测试会失败。
报错信息
Test suite failed to run ReferenceError: fetch is not defined 1 | import { inject, Inject } from '@angular/core'; 2 | import { FirestoreDoc } from './firestore-doc'; > 3 | import { collection, Firestore, getDocs, limit, query } from '@angular/fire/firestore'; | ^ 4 | 5 | @Inject({ providedIn: 'root' }) 6 | export class FirestoreService { at Object.<anonymous> (../../node_modules/firebase/node_modules/@firebase/auth/src/platform_node/index.ts:33:26) at Object.<anonymous> (../../node_modules/firebase/node_modules/@firebase/auth/dist/node/index.js:5:12) at Object.<anonymous> (../../node_modules/firebase/auth/dist/index.cjs.js:5:12) at Object.<anonymous> (../../node_modules/rxfire/auth/index.cjs.js:5:12) at Object.<anonymous> (../../node_modules/@angular/fire/fesm2022/angular-fire-auth.mjs:9:80) at Object.<anonymous> (../../node_modules/@angular/fire/fesm2022/angular-fire-firestore.mjs:8:31) at Object.<anonymous> (../../libs/firestore/src/lib/firestore/firestore.service.ts:3:1) at Object.<anonymous> (../../libs/firestore/src/index.ts:1:1) at Object.<anonymous> (src/app/nx-welcome.component.ts:3:1) at Object.<anonymous> (src/app/nx-wellcome.component.spec.ts:2:1)
相关代码
组件测试代码
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NxWelcomeComponent } from './nx-welcome.component'; describe("NxWelcomeComponent", () => { let component: NxWelcomeComponent; let fixture: ComponentFixture<NxWelcomeComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [NxWelcomeComponent], }).compileComponents(); fixture = TestBed.createComponent(NxWelcomeComponent); component = fixture.componentInstance; }) it("should be created", () => { expect(component).toBeTruthy(); }) })
被测组件代码
import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FirestoreDoc, FirestoreService } from '@angular-monorepo/firestore'; @Component({ selector: 'app-nx-welcome', imports: [CommonModule], template: ` <h2>Firestore Issue</h2> <ul> @for (doc of documents; track doc.name) { <li>{{doc.name}} - {{doc.description}}</li> } </ul> `, styles: [], encapsulation: ViewEncapsulation.None, }) export class NxWelcomeComponent implements OnInit { readonly firestoreService = inject(FirestoreService); documents: FirestoreDoc[] = []; async ngOnInit(): Promise<void> { this.documents = await this.firestoreService.loadAll(); } }
Firestore服务代码
import { inject, Inject } from '@angular/core'; import { FirestoreDoc } from './firestore-doc'; import { collection, Firestore, getDocs, limit, query } from '@angular/fire/firestore'; @Inject({ providedIn: 'root' }) export class FirestoreService { protected readonly firestore = inject(Firestore); private collection; constructor() { this.collection = collection(this.firestore, 'firestore-doc'); } async loadAll(): Promise<FirestoreDoc[]> { const results = await getDocs(query(this.collection, ...[], ...[], limit(99))); const content: FirestoreDoc[] = []; results.docs.map((doc) => { content.push(doc.data() as FirestoreDoc); }); return new Promise<FirestoreDoc[]>((resolve, reject) => { resolve(content); }); } }
项目配置
package.json
{ "name": "@angular-monorepo/source", "version": "0.0.0", "license": "MIT", "scripts": { "run-firebase-emulator": "cd apps/firestore-issue && firebase emulators:start --import ./data", "serve-firestore-issue": "nx run firestore-issue:serve", "run-failing-test": "jest apps/firestore-issue/src/app/nx-wellcome.component.spec.ts" }, "private": true, "dependencies": { "@angular/common": "~19.2.0", "@angular/compiler": "~19.2.0", "@angular/core": "~19.2.0", "@angular/fire": "^19.0.0", "@angular/forms": "~19.2.0", "@angular/platform-browser": "~19.2.0", "@angular/platform-browser-dynamic": "~19.2.0", "@angular/router": "~19.2.0", "rxjs": "~7.8.0", "uuid": "^11.0.5", "zone.js": "~0.15.0" }, "devDependencies": { "@angular-devkit/build-angular": "~19.2.0", "@angular-devkit/core": "~19.2.0", "@angular-devkit/schematics": "~19.2.0", "@angular/cli": "~19.2.0", "@angular/compiler-cli": "~19.2.0", "@angular/language-service": "~19.2.0", "@eslint/js": "^9.8.0", "@nx/angular": "20.5.0", "@nx/devkit": "20.5.0", "@nx/eslint": "20.5.0", "@nx/eslint-plugin": "20.5.0", "@nx/jest": "20.5.0", "@nx/js": "20.5.0", "@nx/playwright": "20.5.0", "@nx/web": "20.5.0", "@nx/workspace": "20.5.0", "@playwright/test": "^1.36.0", "@schematics/angular": "~19.2.0", "@swc-node/register": "~1.9.1", "@swc/core": "~1.5.7", "@swc/helpers": "~0.5.11", "@types/jest": "^29.5.12", "@types/node": "18.16.9", "@typescript-eslint/utils": "^8.19.0", "angular-eslint": "^19.2.0", "autoprefixer": "^10.4.0", "eslint": "^9.8.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-playwright": "^1.6.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-preset-angular": "~14.4.0", "jsonc-eslint-parser": "^2.1.0", "ng-packagr": "~19.2.0", "nx": "20.5.0", "postcss": "^8.4.5", "postcss-url": "~10.1.3", "prettier": "^2.6.2", "ts-jest": "^29.1.0", "ts-node": "10.9.1", "tslib": "^2.3.0", "typescript": "~5.7.2", "typescript-eslint": "^8.19.0" } }
应用tsconfig相关文件
tsconfig.json
{ "compilerOptions": { "target": "es2022", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true }, "files": [], "include": [], "references": [ { "path": "./tsconfig.editor.json" }, { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.spec.json" } ], "extends": "../../tsconfig.base.json", "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } }
tsconfig.spec.json
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", "target": "es2016", "types": ["jest", "node"] }, "files": ["src/test-setup.ts"], "include": [ "jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts" ] }
tsconfig.app.json
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"], "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] }
tsconfig.editor.json
{ "extends": "./tsconfig.json", "include": ["src/**/*.ts"], "compilerOptions": {}, "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] }
解决方案
1. 在测试入口全局注入fetch
Jest的jsdom环境(v29+)不再默认提供fetch API,而Firebase依赖这个API。在src/test-setup.ts中添加全局注入:
// 安装node-fetch@2(适配Node 18以下版本) globalThis.fetch = require('node-fetch'); globalThis.Headers = require('node-fetch').Headers; globalThis.Request = require('node-fetch').Request; globalThis.Response = require('node-fetch').Response; // 或者使用jest-fetch-mock简化配置 // import 'jest-fetch-mock';
确保已经安装依赖:
npm install node-fetch@2 --save-dev # 或 npm install jest-fetch-mock --save-dev
2. 调整Jest配置处理Firebase依赖
在应用的jest.config.ts中,更新配置以避免Jest转换Firebase相关模块,并指定测试入口:
export default { preset: '../../jest.preset.js', setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, transformIgnorePatterns: [ 'node_modules/(?!@angular/fire|firebase|rxfire)' ], moduleFileExtensions: ['ts', 'html', 'js', 'json'], coverageDirectory: '../../coverage/apps/firestore-issue', };
3. 组件测试中模拟FirestoreService
单元测试应隔离外部依赖,直接模拟FirestoreService避免加载真实Firebase代码:
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [NxWelcomeComponent], providers: [ { provide: FirestoreService, useValue: { loadAll: jest.fn().mockResolvedValue([ { name: '测试文档', description: '测试描述' } ]) } } ], }).compileComponents(); });
4. 统一类库与应用的测试环境
检查类库的Jest配置,确保应用侧配置与之对齐,比如是否同样注入了fetch或模拟了Firebase依赖。
内容的提问来源于stack exchange,提问作者Zsolt
相关产品推荐
相关产品推荐

