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

如何用Jest测试继承带泛型基类的Angular组件?

问题描述

我有如下基类:

export class TableBase<TData>
  implements OnInit, OnDestroy, AfterViewInit, OnDestroy
{
  @ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader>;
}

以及如下组件:

export class MasterdataGlobalComponent extends TableBase<IMasterdataGlobalItem> {
  @ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader>;
}

其他测试均正常运行,但遇到如下错误,推测是TData对象相关问题:

NullInjectorError: R3InjectorError(Standalone[MasterdataGlobalComponent])[[object Object] -> [object Object] -> [object Object]]:
  NullInjectorError: No provider for [object Object]!

请问定义/传递该对象的最佳方式是什么?

以下是当前的测试代码:

jest.mock('../../../framework/common/services/auth.service');
import { AuthenticationService } from '../../../framework/common/services/auth.service';
import { ArticleService } from '../../../app/inventory/articles/article.service';
import { HttpClient } from '@angular/common/http';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import {
  TranslateModule,
  TranslateLoader,
  TranslateFakeLoader
} from '@ngx-translate/core';
import { ToastrModule } from 'ngx-toastr';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import {
  MockAuthenticationService,
  MockMasterdataGlobalItem
} from '../../mocks/mocks';

import { MasterdataGlobalComponent } from './masterdata-global.component';
import { MasterdataGlobalItemsService } from './masterdata-global-items-service';
import { TableBase } from '../../../framework/common/table/table-base';
import { IMasterdataGlobalItem } from './masterdata-global-item.interface';
import { NgbdSortableHeader } from '../../../framework/common/table/sortable.directive';

// jest.mock('../../../framework/common/services/auth.service', () => ({
//   AuthenticationService: {
//     currentUser$: new BehaviorSubject<any>({
//       config: [
//         { setting: 'rowsPerPage', value: '10' } // Adjust as needed
//       ]
//     })
//   }
// }));

class MockTableBase extends TableBase<any> {
  constructor() {
    super(null, null, '', null);
  }

  // Override methods or properties used by MasterdataGlobalComponent here
  // For example:
  ngOnInit() {}
  ngOnDestroy() {}
  // Implement other methods or properties as needed
}

// Mocking the PlantSwitched observable
const mockPlantSwitched = new BehaviorSubject<any>(null);

// Mocking the UserPlantsService
jest.mock('../../../framework/common/services/user-plants.service', () => ({
  UserPlantsService: jest.fn().mockImplementation(() => ({
    activatedUserPlant$: new BehaviorSubject<any>(null)
  }))
}));

jest.mock('../../../app/inventory/articles/article.service', () => ({
  ArticleService: {
    // Mocked methods or properties as needed
  }
}));

// jest.mock('../../../framework/common/table/table-base', () => ({
//   TableBase: MockTableBase
// }));

// Mock data for testing
const mockData: IMasterdataGlobalItem[] = [
  // Define mock items here
];
describe('MasterdataGlobalComponent', () => {
  let component: MasterdataGlobalComponent;
  let fixture: ComponentFixture<MasterdataGlobalComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        MasterdataGlobalComponent,
        NgbdSortableHeader,
        HttpClientTestingModule,
        TranslateModule.forRoot({
          loader: {
            provide: TranslateLoader,
            useClass: TranslateFakeLoader
          }
        }),
        ToastrModule.forRoot()
      ],
      providers: [
        { provide: AuthenticationService, useClass: MockAuthenticationService },
        provideMockStore({}),
        {
          provide: MasterdataGlobalItemsService,
          useValue: MasterdataGlobalItemsService
        },

        { provide: TableBase, useClass: MockTableBase }
      ]
    }).compileComponents();
  }));

  beforeEach(() => {
    (window as any).PlantSwitched = mockPlantSwitched;
    const dataSubject = new BehaviorSubject<IMasterdataGlobalItem[]>([]);
    Object.defineProperty(MasterdataGlobalComponent.prototype, 'data$', {
      get: jest.fn(() => dataSubject.asObservable())
    });

    fixture = TestBed.createComponent(MasterdataGlobalComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    // Set the mock data
    dataSubject.next(mockData);
    fixture.detectChanges();
  });

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

这个基类是该组件与其他测试正常组件的唯一区别。


解决方案

首先明确:错误和TData泛型本身无关——泛型是类型层面的定义,运行时不会存在。问题核心在于基类TableBase的依赖注入配置错误,以及测试中的几个不当设置,以下是修复步骤:

1. 清理测试中错误的Provider配置

  • 移除providers中的{ provide: TableBase, useClass: MockTableBase }:子类继承基类时,Angular会自动解析基类的依赖,单独注册基类Provider会导致注入冲突。
  • 修正MasterdataGlobalItemsService的Provider:你当前用useValue: MasterdataGlobalItemsService是把类本身作为值传递,应该传入mock实例:
    {
      provide: MasterdataGlobalItemsService,
      useValue: {
        // 模拟组件需要的服务方法,比如:
        getGlobalItems: jest.fn().mockReturnValue(new BehaviorSubject<IMasterdataGlobalItem[]>(mockData).asObservable())
      }
    }
    

2. 正确模拟基类的依赖与行为

如果TableBase的构造函数需要依赖(比如你代码中super(null, null, '', null)暗示的4个参数),要在测试中为这些依赖提供对应的mock:

  • 若依赖是Angular内置服务(如HttpClient),已由HttpClientTestingModule提供,无需额外处理;
  • 若依赖是自定义服务,需在providers中添加对应的mock。

另外,可直接用jest.mock替换整个TableBase,避免手动继承模拟:

jest.mock('../../../framework/common/table/table-base', () => ({
  TableBase: jest.fn().mockImplementation(() => ({
    headers: new QueryList<NgbdSortableHeader>(),
    ngOnInit: jest.fn(),
    ngOnDestroy: jest.fn(),
    ngAfterViewInit: jest.fn()
  }))
}));

3. 完善泛型相关的测试数据

虽然泛型不影响运行,但为了测试完整性,补充符合IMasterdataGlobalItem类型的mock数据:

const mockData: IMasterdataGlobalItem[] = [
  { id: 1, name: '测试项1' },
  { id: 2, name: '测试项2' }
];

根据你的实际接口字段调整内容。

4. 修正其他测试细节

  • 移除imports中的NgbdSortableHeader:指令不需要直接导入到测试模块,若它属于某个共享模块,导入该模块即可;
  • 去掉Object.defineProperty对data$的覆盖:如果data$是从MasterdataGlobalItemsService获取,通过mock服务返回数据即可,无需手动覆盖原型属性。

修正后的核心测试配置示例

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [
      MasterdataGlobalComponent,
      HttpClientTestingModule,
      TranslateModule.forRoot({
        loader: {
          provide: TranslateLoader,
          useClass: TranslateFakeLoader
        }
      }),
      ToastrModule.forRoot()
    ],
    providers: [
      { provide: AuthenticationService, useClass: MockAuthenticationService },
      provideMockStore({}),
      {
        provide: MasterdataGlobalItemsService,
        useValue: {
          getGlobalItems: jest.fn().mockReturnValue(new BehaviorSubject<IMasterdataGlobalItem[]>(mockData).asObservable())
        }
      }
    ]
  }).compileComponents();
}));

beforeEach(() => {
  (window as any).PlantSwitched = mockPlantSwitched;
  fixture = TestBed.createComponent(MasterdataGlobalComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 19:43:13