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

Angular组件测试遇Undefined错误及mock服务作用疑问

编写Angular组件测试时的问题与疑问

遇到的两类错误

  • 初始运行测试时报错:TypeError: Cannot read properties of undefined (reading 'fetchPersonnelList')
    原因:测试代码中声明了service: PersonnelDataService但未初始化,直接访问service.fetchPersonnelList导致读取undefined的属性。
  • 注入PersonnelDataService后报错:Error: <toHaveBeenCalled> : Expected a spy, but got Function
    原因:mockPersonnelDataService里的fetchPersonnelList是普通函数,而toHaveBeenCalled只能用于断言间谍函数的调用情况,普通函数不支持这个断言。

改用spyOn创建间谍函数后测试通过,针对测试过程中的几个疑问解答如下:

疑问解答

1. 自定义的mockPersonnelDataService有何作用?

  • 替代真实的PersonnelDataService,隔离组件与后端接口/复杂服务逻辑的依赖,让测试只关注组件自身的行为。
  • 提供固定的模拟数据(比如list$返回预设的testHFSTaleSchema.rows),确保测试环境稳定,不会因为真实数据变化导致测试失败。
  • 模拟服务的基础方法,满足组件初始化和运行时的依赖需求,比如组件需要监听list$或调用setPageSize时,mock服务能提供符合预期的响应。

2. 如何验证mock数据?

  • 验证UI渲染:触发组件变更检测后,获取模板中的列表元素,检查数量或内容是否与mock数据一致。
  • 验证组件内部数据:断言组件接收mock数据后的内部属性(比如personnelList)是否与预设mock值匹配。

示例代码:

it('should display mock personnel data', fakeAsync(() => {
  component.ngOnInit();
  tick();
  flush();
  fixture.detectChanges(); // 更新UI
  const listItems = fixture.nativeElement.querySelectorAll('.personnel-item');
  expect(listItems.length).toBe(testHFSTaleSchema.rows.length);
  // 断言组件内部数据
  expect(component.personnelList).toEqual(testHFSTaleSchema.rows);
}));

3. 是否可以直接在returnValue中传入数据?

完全可以,只要数据格式符合组件的预期即可:

  • 如果服务方法返回Observable,需要用of()把数据包装成Observable(比如and.returnValue(of(testHFSTaleSchema.rows))),否则组件订阅时会报错。
  • 你修改代码中用and.returnValue(of([]))返回空数组,也可以直接传入真实的mock数据,满足不同测试场景的需求。

原始测试代码

import { HttpClientModule } from '@angular/common/http';
import { Component } from '@angular/core';
import {
    async,
    ComponentFixture,
    fakeAsync,
    flush,
    TestBed,
    tick,
} from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { SharedModule } from '../../../../../shared/shared.module';
import { PersonnelDataService } from '../services/personnel-data.service';
import { testHFSTaleSchema } from './hfs-table.schema';
import { ListPersonnelComponent } from './list-personnel.component';

const mockPersonnelDataService = {
    list$: of(testHFSTaleSchema.rows),
    fetchPersonnelList: function () {
        return { subscribe: () => this.list$ };
    },
    setPageSize: function () {
        return this.list$;
    },
    fetchPaginatedList: () => {},
};

@Component({
    selector: 'app-details-personnel',
    template: '',
})
export class PersonnelDetailsComponent {}

fdescribe('ListPersonnelComponent', () => {
    let component: ListPersonnelComponent;
    let fixture: ComponentFixture<ListPersonnelComponent>;
    let activatedRoute = { url: {} } as ActivatedRoute;
    let service: PersonnelDataService;
    let router;
    const routes = [
        {
            path: 'personnel-details',
            component: PersonnelDetailsComponent,
        },
    ];

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [
                RouterTestingModule.withRoutes(routes),
                SharedModule,
                BrowserAnimationsModule,
                HttpClientModule,
            ],
            declarations: [ListPersonnelComponent],
            providers: [
                {
                    provide: PersonnelDataService,
                    useValue: mockPersonnelDataService,
                },
                { provide: ActivatedRoute, useValue: activatedRoute },
            ],
        }).compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(ListPersonnelComponent);
        component = fixture.componentInstance;
        router = TestBed.inject(Router);
        fixture.detectChanges();
    });

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

    it('should call fetchPersonnelList on page Oninit', fakeAsync(() => {
        component.ngOnInit();
        tick(1000);
        expect(service.fetchPersonnelList).toHaveBeenCalled();//error
        flush();
    }));
});

修改后的测试用例代码

it('should call fetchPersonnelList on page Oninit', fakeAsync(() => {
        const fetchPersonnelListSpy = spyOn(
            service,
            'fetchPersonnelList'
        ).and.returnValue(of([]));
        component.ngOnInit();
        tick(1000);
        expect(fetchPersonnelListSpy).toHaveBeenCalled(); 
        flush();
    }));

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 09:31:34