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

如何在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); // 失败
  });
});

问题原因

  1. 未模拟路由参数:组件通过getTrainerId()从ActivatedRoute获取ID,但测试中未提供该依赖的mock,导致获取的ID无效,getTrainerById无法触发正确的回调。
  2. 缺少服务mock:courseService.getAllCourses()在测试中未被mock,其订阅流程异常可能阻断后续表单填充逻辑。
  3. 手动赋值干扰初始化:测试前手动给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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 13:02:04