升级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
相关产品推荐
相关产品推荐

