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

