You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 03:25:56