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

Angular Jest单元测试报错:TypeError: 无法读取undefined的subscribe属性

Angular Jest测试报错:TypeError: Cannot read properties of undefined (reading 'subscribe')

我是Angular Jest测试新手,在编写EhiExportPendingComponent的单元测试时遇到报错:TypeError: Cannot read properties of undefined (reading 'subscribe')。该组件使用@ngrx/store的Store类、EhiExportCommunicationService等服务,在ngOnInit及cancelRequest方法中存在多处subscribe逻辑。

组件代码

@Component({
    selector: 'tw-ehi-export-pending',
    templateUrl: './ehi-export-pending.component.html'
})
export class EhiExportPendingComponent implements OnInit, OnDestroy {
    subs = new SubSink();
    gridData: any[] = [];
    orgName = '';

    constructor(
        private ehiexportstore: Store < ehiexportreducer.EHIExportState > ,
        private ehiExportCommService: EhiExportCommunicationService,
        private hostComm: HostCommunicationService,
        private ehiExportService: EhiExportService,
        private cdr: ChangeDetectorRef
    ) {}

    ngOnInit(): void {
        this.orgName = this.hostComm.getUser().organization;

        this.subs.sink = this.ehiexportstore.pipe(select(ehiexportreducer.getPendingRequests)).subscribe((data) => {
            this.gridData = data ? [...data] : [];
            this.cdr.detectChanges();
        });

        this.subs.sink = this.ehiExportCommService.selectedSideMenuOption$.subscribe((selectedMenu: string) => {
            if (selectedMenu.toLocaleLowerCase() === 'pending') {
                this.ehiexportstore.dispatch(EHIExportActions.LoadPendingRequests());
            }
        });

        this.subs.sink = this.ehiExportCommService.pendingDataRefresh$.subscribe(() => {
            this.ehiexportstore.dispatch(EHIExportActions.LoadPendingRequests());
        });
        this.ehiexportstore.dispatch(EHIExportActions.LoadPendingRequests());
    }

    cancelRequest(requestId: number): void {
        this.hostComm.cancelExportDialog().then((choice) => {
            if (choice === 'continue') {
                this.ehiExportService.cancelRequest(requestId).subscribe({
                    next: (): void => this.ehiExportCommService.triggerPendingDataRefresh(),
                    error: (): void => {
                        console.log(`Something went wrong while canceling the request with request ID ${requestId}.`);
                        this.ehiExportCommService.triggerPendingDataRefresh();
                    }
                });
            }
        });
    }

    ngOnDestroy(): void {
        this.subs.unsubscribe();
    }
}

测试代码

describe('EhiExportPendingComponent', () => {
    let component: EhiExportPendingComponent;
    let fixture: ComponentFixture < EhiExportPendingComponent > ;
    let mockStore: MockStore < ehiexportreducer.EHIExportState > ;

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            providers: [
                provideMockStore(),
                {
                    provide: EhiExportCommunicationService,
                    useValue: new MockEhiExportCommunicationService()
                },
                {
                    provide: HostCommunicationService,
                    useValue: new MockHostCommunicationService()
                },
                {
                    provide: EhiExportService,
                    useValue: EhiExportService
                }
            ],
            declarations: [EhiExportPendingComponent]
        }).compileComponents();

        fixture = TestBed.createComponent(EhiExportPendingComponent);
        mockStore = TestBed.get(Store);

        component = fixture.componentInstance;

        mockStore.overrideSelector(ehiexportreducer.getPendingRequests, pendingRequests);
        fixture.detectChanges();
    });

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

问题原因分析

  1. EhiExportService的Mock错误:测试代码中直接将EhiExportService类作为useValue传入,组件注入的是类本身而非实例,调用cancelRequest时会得到undefined,触发subscribe报错。
  2. MockEhiExportCommunicationService可能未正确实现Observable属性:如果这个mock类没有将selectedSideMenuOption$和pendingDataRefresh$定义为可订阅的Observable对象,组件在ngOnInit中订阅时也会报错。
  3. MockHostCommunicationService的getUser方法可能未返回有效对象:虽然当前报错是subscribe相关,但如果getUser()返回undefined,也会导致organization读取失败。

修复方案

1. 正确Mock EhiExportService

将EhiExportService的mock改为返回包含cancelRequest方法的对象,该方法返回一个可订阅的Observable:

{
    provide: EhiExportService,
    useValue: {
        cancelRequest: jest.fn(() => of({})) // of()来自rxjs,返回空的成功Observable
    }
}

2. 完善MockEhiExportCommunicationService

确保mock类中的Observable属性正确初始化,同时实现triggerPendingDataRefresh方法:

class MockEhiExportCommunicationService {
    // 用BehaviorSubject确保有初始值,避免订阅时无数据
    selectedSideMenuOption$ = new BehaviorSubject<string>('pending');
    pendingDataRefresh$ = new BehaviorSubject<void>(undefined);
    triggerPendingDataRefresh = jest.fn(() => this.pendingDataRefresh$.next());
}

3. 完善MockHostCommunicationService

确保getUser()返回包含organization的对象,同时实现cancelExportDialog方法:

class MockHostCommunicationService {
    getUser = jest.fn(() => ({ organization: '测试组织' }));
    cancelExportDialog = jest.fn(() => Promise.resolve('continue')); // 默认返回continue,方便测试cancelRequest逻辑
}

4. 优化测试代码中的Store获取方式

使用Angular推荐的TestBed.inject替代TestBed.get:

mockStore = TestBed.inject(Store) as MockStore<ehiexportreducer.EHIExportState>;

修复后的完整测试代码

import { TestBed, ComponentFixture } from '@angular/core/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import { EhiExportPendingComponent } from './ehi-export-pending.component';
import { EhiExportCommunicationService } from '../services/ehi-export-communication.service';
import { HostCommunicationService } from '../services/host-communication.service';
import { EhiExportService } from '../services/ehi-export.service';
import { ehiexportreducer } from '../store/reducers/ehi-export.reducer';
import { BehaviorSubject, of } from 'rxjs';

class MockEhiExportCommunicationService {
    selectedSideMenuOption$ = new BehaviorSubject<string>('pending');
    pendingDataRefresh$ = new BehaviorSubject<void>(undefined);
    triggerPendingDataRefresh = jest.fn(() => this.pendingDataRefresh$.next());
}

class MockHostCommunicationService {
    getUser = jest.fn(() => ({ organization: '测试组织' }));
    cancelExportDialog = jest.fn(() => Promise.resolve('continue'));
}

describe('EhiExportPendingComponent', () => {
    let component: EhiExportPendingComponent;
    let fixture: ComponentFixture<EhiExportPendingComponent>;
    let mockStore: MockStore<ehiexportreducer.EHIExportState>;
    let mockEhiExportService: typeof EhiExportService;

    // 模拟pending请求数据
    const pendingRequests = [{ id: 1, name: '测试请求' }];

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            providers: [
                provideMockStore(),
                {
                    provide: EhiExportCommunicationService,
                    useValue: new MockEhiExportCommunicationService()
                },
                {
                    provide: HostCommunicationService,
                    useValue: new MockHostCommunicationService()
                },
                {
                    provide: EhiExportService,
                    useValue: {
                        cancelRequest: jest.fn(() => of({}))
                    }
                }
            ],
            declarations: [EhiExportPendingComponent]
        }).compileComponents();

        fixture = TestBed.createComponent(EhiExportPendingComponent);
        mockStore = TestBed.inject(Store) as MockStore<ehiexportreducer.EHIExportState>;
        mockEhiExportService = TestBed.inject(EhiExportService);

        component = fixture.componentInstance;

        mockStore.overrideSelector(ehiexportreducer.getPendingRequests, pendingRequests);
        fixture.detectChanges();
    });

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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 04:17:08