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

升级Angular/NgRx/Jest后,含NgRx Entity Service组件的单元测试失败

升级Angular/NgRx后NgRx Entity Service单元测试失效问题

问题背景

升级项目至Angular 15.2.4、Jest 29.5和NgRx 15.4后,原本正常的包含NgRx Entity Service的组件单元测试失效,报错TypeError: serviceElementsFactory.create is not a function。

实体服务代码

import { Injectable } from '@angular/core';
import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory } from '@ngrx/data';
import { Site } from 'app/models/site.model';

@Injectable({ providedIn: 'root' })
export class SiteEntityService extends EntityCollectionServiceBase<Site> {
  constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
    super('Site', serviceElementsFactory);
  }
}

组件代码

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Site } from 'app/models/site.model';
import { SiteEntityService } from 'app/store/site-entity.service';
import { Breadcrumb } from 'app/theme/shared/components/breadcrumb/models/breadcrumb.model';
import { BreadcrumbService } from 'app/theme/shared/components/breadcrumb/services/breadcrumb.service';

@Component({
  selector: 'app-site-search-shell',
  templateUrl: './site-search-shell.component.html',
  styleUrls: ['./site-search-shell.component.scss'],
})
export class SiteSearchShellComponent implements OnInit {
  sites$: Observable<Site[]>;

  constructor(
    private siteEntityService: SiteEntityService,
    private breadCrumbService: BreadcrumbService,
    private router: Router,
  ) {
    this.sites$ = siteEntityService.entities$;
    this.breadCrumbService.add(new Breadcrumb('Sites', '/sites'));
  }

  ngOnInit(): void {
    this.siteEntityService.getWithQuery('$sort_by=name');
  }

  addClicked(): void {
    this.router.navigateByUrl('/sites/create');
  }
}

失效的测试代码

import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MockComponent } from 'ng-mocks';

import { SiteSearchDisplayComponent } from '../site-search-display/site-search-display.component';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { BreadcrumbService } from 'app/theme/shared/components/breadcrumb/services/breadcrumb.service';
import { SiteSearchShellComponent } from './site-search-shell.component';
import { of } from 'rxjs';
import { EntityCollection, EntityCollectionServiceBase, EntityCollectionServiceElements, EntityCollectionServiceElementsFactory, EntityDefinitionService, EntityDispatcherFactory, EntitySelectors$Factory, EntitySelectorsFactory } from '@ngrx/data';
import { Site } from 'app/models/site.model';
import { provideMockStore } from '@ngrx/store/testing';
import { SiteEntityService } from 'app/store/site-entity.service';

describe('SiteSearchShellComponent', () => {
  let component: SiteSearchShellComponent;
  let fixture: ComponentFixture<SiteSearchShellComponent>;
  let service: SiteEntityService;
  let router: Router;

  beforeEach(
    async () => {
      TestBed.configureTestingModule({
        declarations: [
          SiteSearchShellComponent,
          MockComponent(SiteSearchDisplayComponent)
        ],
        providers: [
          SiteEntityService,
          {
            provide: EntityCollectionServiceElementsFactory,
            useValue: {
              methodsToFake: jest.fn().mockImplementation(() => {}),
            }
          },
          BreadcrumbService
        ],
        imports: [RouterTestingModule],
        teardown: { destroyAfterEach: false }
      }).compileComponents();
      service = TestBed.inject(SiteEntityService);

      router = TestBed.inject(Router);
    });


  beforeEach(() => {
    fixture = TestBed.createComponent(SiteSearchShellComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });



  test('should create', () => {
    expect(component).toBeTruthy();
  });

  test('should route to sites/create when addCreate event called', () => {
    jest.spyOn(component, 'addClicked');
    const navigateSpy = spyOn(router, 'navigateByUrl');
    component.addClicked();
    fixture.detectChanges();
    expect(component.addClicked).toBeCalledTimes(1);
    expect(navigateSpy).toBeCalledTimes(1);
    expect(navigateSpy).toBeCalledWith('/sites/create');      
  });
});

报错信息

● SiteSearchShellComponent › should create

    TypeError: serviceElementsFactory.create is not a function

       6 | export class SiteEntityService extends EntityCollectionServiceBase<Site> {
       7 |   constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
    >  8 |     super('Site', serviceElementsFactory);
         |     ^
       9 |   }
      10 | }
      11 |

      at new EntityCollectionServiceBase (node_modules/@ngrx/data/fesm2020/ngrx-data.mjs:2221:78)
      at new SiteEntityService (src/app/store/site-entity.service.ts:8:5)
      at Object.SiteEntityService_Factory [as factory] (ng:\SiteEntityService\ɵfac.js:5:10)
      at R3Injector.hydrate (node_modules/@angular/core/fesm2020/core.mjs:8031:35)
      at R3Injector.get (node_modules/@angular/core/fesm2020/core.mjs:7919:33)
      at R3Injector.<anonymous> (node_modules/ng-mocks/webpack:/ng-mocks/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts:371:53)
      at R3Injector.this (node_modules/ng-mocks/webpack:/ng-mocks/libs/ng-mocks/src/lib/mock-service/helper.create-clone.ts:10:65)
      at TestBedImpl.inject (node_modules/@angular/core/fesm2020/testing.mjs:24201:52)
      at Function.inject (node_modules/@angular/core/fesm2020/testing.mjs:24060:37)
      at src/app/areas/sites/site-search/site-search-shell/site-search-shell.component.spec.ts:41:25
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:416:30)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:3728:43)
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:415:56)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:173:47)
      at Object.wrappedFunc (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:4208:34)

解决方案

报错核心原因是测试中对EntityCollectionServiceElementsFactory的mock不完整,EntityCollectionServiceBase的构造函数会调用该工厂的create()方法,原测试未提供这个关键方法的mock实现。

方案一:补全底层工厂mock

修改测试中的EntityCollectionServiceElementsFactory provider,让它返回包含create方法的对象,该方法返回组件依赖的必要属性:

{
  provide: EntityCollectionServiceElementsFactory,
  useValue: {
    create: jest.fn(() => ({
      entities$: of([]), // mock实体数据流
      getWithQuery: jest.fn() // mock需要调用的方法
    }))
  }
}

方案二:直接mock SiteEntityService(更推荐)

组件依赖的是SiteEntityService,直接mock这个服务无需关心底层NgRx细节,测试逻辑更聚焦:

beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [
      SiteSearchShellComponent,
      MockComponent(SiteSearchDisplayComponent)
    ],
    providers: [
      BreadcrumbService,
      // 直接mock SiteEntityService
      {
        provide: SiteEntityService,
        useValue: {
          entities$: of([]),
          getWithQuery: jest.fn()
        }
      }
    ],
    imports: [RouterTestingModule],
    teardown: { destroyAfterEach: false }
  }).compileComponents();

  router = TestBed.inject(Router);
});

修改后的完整测试代码:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MockComponent } from 'ng-mocks';

import { SiteSearchDisplayComponent } from '../site-search-display/site-search-display.component';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { BreadcrumbService } from 'app/theme/shared/components/breadcrumb/services/breadcrumb.service';
import { SiteSearchShellComponent } from './site-search-shell.component';
import { of } from 'rxjs';
import { SiteEntityService } from 'app/store/site-entity.service';

describe('SiteSearchShellComponent', () => {
  let component: SiteSearchShellComponent;
  let fixture: ComponentFixture<SiteSearchShellComponent>;
  let siteEntityService: jest.Mocked<SiteEntityService>;
  let router: Router;

  beforeEach(async () => {
    const mockSiteEntityService = {
      entities$: of([]),
      getWithQuery: jest.fn()
    };

    await TestBed.configureTestingModule({
      declarations: [
        SiteSearchShellComponent,
        MockComponent(SiteSearchDisplayComponent)
      ],
      providers: [
        BreadcrumbService,
        { provide: SiteEntityService, useValue: mockSiteEntityService }
      ],
      imports: [RouterTestingModule],
      teardown: { destroyAfterEach: false }
    }).compileComponents();

    siteEntityService = TestBed.inject(SiteEntityService) as jest.Mocked<SiteEntityService>;
    router = TestBed.inject(Router);
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(SiteSearchShellComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  test('should create', () => {
    expect(component).toBeTruthy();
  });

  test('should call getWithQuery on init', () => {
    expect(siteEntityService.getWithQuery).toHaveBeenCalledWith('$sort_by=name');
  });

  test('should route to sites/create when addClicked is called', () => {
    const navigateSpy = jest.spyOn(router, 'navigateByUrl');
    component.addClicked();
    expect(navigateSpy).toHaveBeenCalledTimes(1);
    expect(navigateSpy).toHaveBeenCalledWith('/sites/create');
  });
});

该方案不仅解决了报错,还新增了对ngOnInit中调用getWithQuery的测试,覆盖了更多组件逻辑。

内容的提问来源于stack exchange,提问作者John McArthur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 22:47:06