如何在Angular中测试ngOnInit?表单预填充测试失败排查
Angular Jasmine测试:Trainer编辑表单未预填充数据
我使用Angular和Jasmine开发Trainer实体的编辑表单,组件的ngOnInit方法会从URL中获取Trainer的ID,再调用服务从数据库拉取数据填充表单。但测试时验证firstName字段始终报错:
Expected null to be 'Devin'
打印trainerForm.value发现所有字段都是初始默认值:
LOG: Object{firstName: null, lastName: null, email: '', officeLocation: '', pathwaysControl: [], courses: [], skills: null}
组件核心代码
export class EditTrainerComponent implements OnInit { trainerForm!: FormGroup; trainer: Trainer = emptyTrainer(); constructor( private formBuilder: FormBuilder, private trainerService: TrainerService, private officeLocationService: OfficeLocationService, private courseService: CourseService, private pathwayService: PathwayService, private route: ActivatedRoute, // 其他依赖 ) { this.trainerForm = this.formBuilder.group({ firstName: [null, [Validators.required, Validators.minLength(2)]], lastName: [null, [Validators.required, Validators.minLength(2)]], email: ['', [Validators.required, Validators.pattern('^[a-zA-Z0-9._%+-]+@(?:[a-zA-Z0-9-]+\\.)?fdmgroup.com$')]], officeLocation: ['', Validators.required], pathwaysControl: [[]], courses: [[]], skills: [null, Validators.required], }); } ngOnInit(): void { this.trainerService.getAllSkills().subscribe((skills) => { this.skills = skills; }); this.officeLocationService.getAllOfficeLocations().subscribe((offices) => { this.offices = offices; }); this.courseService.getAllCourses().subscribe((courses) => { this.courses = courses; this.allCourses = courses; }); this.pathwayService.getAllPathways().subscribe((pathways) => { this.pathways = pathways; const trainerId = this.getTrainerId(); this.trainerService.getTrainerById(trainerId).subscribe((trainer) => { this.trainer = trainer; this.trainerForm.patchValue({ firstName: this.trainer.firstName, lastName: this.trainer.lastName, email: this.trainer.email, officeLocation: this.trainer.officeLocation?.city, pathwaysControl: this.getPathwaysByCourses(this.trainer.coursesEligibleToTeach), courses: this.getCourseNames(this.trainer.coursesEligibleToTeach), skills: this.getSkillNames(this.trainer.skills), }); }); }); } private getTrainerId(): number { return Number(this.route.snapshot.paramMap.get('userId')); } }
测试核心代码
describe('EditTrainerComponent', () => { let component: EditTrainerComponent; let fixture: ComponentFixture<EditTrainerComponent>; let mockTrainerService: jasmine.SpyObj<TrainerService>; let mockCourseService: jasmine.SpyObj<CourseService>; let mockPathwayService: jasmine.SpyObj<PathwayService>; let mockTrainer: Trainer; let mockSkills: Skill[] = []; let mockPathways: Pathway[] = []; let mockCourses: Course[] = []; beforeEach(async () => { mockTrainer = { userId: 14, firstName: "Devin", lastName: "Troy", email: "devin.troy@fdmgroup.com", username: "devin.troy", userType: { userTypeId: 2, name: "Trainer" }, officeLocation: { id: 5, city: "Charlotte", stateOrProvince: "NC", country: { countryId: 2, countryName: "United States", region: { id: 1, regionName: "North America", regionCode: "NA" }, countryCode: "USA" } }, dateJoined: new Date("2020-08-12T00:22:10.000+00:00"), bio: "", trainerStatus: "active", trainerRoles: [], coursesEligibleToTeach: [], skills: [ { id: 22, name: "Excel" }, { id: 5, name: "APM" } ], dateModified: new Date("2026-01-01T00:00:00.000+00:00") }; mockSkills = []; mockPathways = []; mockCourses = []; mockTrainerService = jasmine.createSpyObj('TrainerService', ['checkIfEmailExists', 'getAllSkills', 'getTrainerById']); mockCourseService = jasmine.createSpyObj('CourseService', ['getCoursesByPathwayId']); mockPathwayService = jasmine.createSpyObj('PathwayService', ['getAllPathways']); mockCourseService.getCoursesByPathwayId.and.returnValue(of(mockCourses)); mockTrainerService.getAllSkills.and.returnValue(of(mockSkills)); mockTrainerService.checkIfEmailExists.and.returnValue(of(false)); mockPathwayService.getAllPathways.and.returnValue(of(mockPathways)); mockTrainerService.getTrainerById.and.returnValue(of(mockTrainer)); await TestBed.configureTestingModule({ declarations: [EditTrainerComponent], imports: [ ReactiveFormsModule, HttpClientTestingModule, RouterModule.forRoot([]), MatFormFieldModule, MatInputModule, MatSelectModule, NoopAnimationsModule, ], providers: [ { provide: TrainerService, useValue: mockTrainerService }, { provide: Location, useClass: SpyLocation }, ], }).compileComponents(); fixture = TestBed.createComponent(EditTrainerComponent); component = fixture.componentInstance; component.trainer = mockTrainer; component.pathways = mockPathways; component.courses = mockPathways[0]?.courses || []; fixture.detectChanges(); }); fit('should show values for existing trainer', async() => { mockTrainerService.getTrainerById.and.returnValue(of(mockTrainer)); fixture.detectChanges(); await fixture.whenStable(); console.log(component.trainerForm.value); expect(component.trainerForm.get('firstName')?.value).toBe(mockTrainer.firstName); // 失败 }); });
问题原因
- 未模拟路由参数:组件通过
getTrainerId()从ActivatedRoute获取ID,但测试中未提供该依赖的mock,导致获取的ID无效,getTrainerById无法触发正确的回调。 - 缺少服务mock:
courseService.getAllCourses()在测试中未被mock,其订阅流程异常可能阻断后续表单填充逻辑。 - 手动赋值干扰初始化:测试前手动给
component.trainer等属性赋值,但组件的patchValue是在服务订阅回调中执行的,手动赋值不会触发表单填充。
解决方案
1. 模拟ActivatedRoute提供有效ID
在测试的providers中添加ActivatedRoute的mock,匹配组件中参数名:
providers: [ { provide: TrainerService, useValue: mockTrainerService }, { provide: Location, useClass: SpyLocation }, { provide: ActivatedRoute, useValue: { snapshot: { paramMap: new Map([['userId', '14']]) } } }, ]
2. 补全所有服务的mock
给CourseService添加getAllCourses的mock:
mockCourseService = jasmine.createSpyObj('CourseService', ['getCoursesByPathwayId', 'getAllCourses']); mockCourseService.getAllCourses.and.returnValue(of(mockCourses));
3. 移除手动赋值代码
删掉beforeEach中手动给组件属性赋值的代码,让组件通过正常初始化流程获取数据:
// 删除以下代码 component.trainer = mockTrainer; component.pathways = mockPathways; component.courses = mockPathways[0]?.courses || [];
4. 确保异步操作完成
使用fakeAsync和tick更可靠地等待所有异步订阅完成:
fit('should show values for existing trainer', fakeAsync(() => { fixture.detectChanges(); // 触发ngOnInit tick(); // 推进时间,完成所有异步订阅 fixture.detectChanges(); // 更新组件状态 expect(component.trainerForm.get('firstName')?.value).toBe(mockTrainer.firstName); // 验证通过 }));
内容的提问来源于stack exchange,提问作者ceeb
相关产品推荐
相关产品推荐

