Angular 11测试:detectChanges与Mock服务配置的相关疑问
Angular 11组件测试常见问题解答
问题场景
在Angular 11的组件测试中,当取消注释第一个fixture.detectChanges()调用时,测试无法得到预期结果。此前收到两点建议:
- 在创建组件前配置Mock服务:确保组件创建时已使用Mock服务
- 配置Mock后触发变更检测:强制组件基于更新后的Mock数据重新渲染
技术疑问
- 为什么会出现上述测试不符合预期的情况?
- 是否存在不依赖输入变更即可强制触发变更检测的方法?
- 若希望在
beforeEach()中调用detectChanges(),是否无法在特定测试用例中配置Mock? - 在每个测试用例(
it)中调用createComponent是否属于最佳实践?
相关代码
测试代码
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MyComponent } from './my.component'; import { SomeService } from '../services/some.service'; describe('MyComponent', () => { let component: MyComponent; let fixture: ComponentFixture<MyComponent>; let compiled; beforeEach(async () => { const spy = jasmine.createSpyObj('SomeService', ['aMethod']); await TestBed.configureTestingModule({ declarations: [ MyComponent ], providers: [ { provide: SomeService, useValue: spy } ] }) .compileComponents(); }); it('should show another mock', () => { // 这段代码从beforeEach中移到了这里 fixture = TestBed.createComponent(MyComponent); component = fixture.componentInstance; compiled = fixture.nativeElement; // *** 如果取消注释这行,Mock值不会被读取 *** // fixture.detectChanges(); const mock: jasmine.SpyObj<SomeService> = TestBed.inject(SomeService) as jasmine.SpyObj<SomeService>;; mock.aMethod.and.returnValue('MOCKED VALUE'); fixture.detectChanges(); expect(compiled.querySelector('p').innerHTML).toContain('MOCKED VALUE'); }); });
组件代码
import { Component, OnInit } from '@angular/core'; import { SomeService } from '../services/some.service'; @Component({ selector: 'app-my', template: ` <p> myPropIs = {{ propThatReadsFromService }} </p> `, styles: [ ] }) export class MyComponent implements OnInit { propThatReadsFromService: string = 'not set yet'; constructor(private someService: SomeService) { } ngOnInit(): void { this.propThatReadsFromService = this.someService.aMethod() || 'did not pick value from service'; } }
问题解答
1. 为什么测试不符合预期?
Angular组件的初始化生命周期钩子(如ngOnInit)会在第一次调用fixture.detectChanges()时触发。如果先调用detectChanges(),此时Mock服务的aMethod还未设置返回值,默认返回undefined,ngOnInit执行时会把propThatReadsFromService设为'did not pick value from service'。后续再配置Mock返回值并调用detectChanges(),由于ngOnInit只会执行一次,组件不会重新读取服务数据,视图也就不会更新为预期的'MOCKED VALUE'。
2. 不依赖输入变更强制触发变更检测的方法?
有两种常用方案:
- 直接调用
fixture.detectChanges():触发组件完整的变更检测周期,同步更新视图。 - 注入
ChangeDetectorRef操作:在组件实例中注入ChangeDetectorRef,调用cdr.detectChanges()可触发局部变更检测;调用cdr.markForCheck()会标记组件为需要检测,等待下一次变更检测周期执行。
3. 在beforeEach()中调用detectChanges(),能否在特定测试用例中配置Mock?
可以,但需调整Mock配置时机:
- 若必须在
beforeEach()中调用detectChanges(),可在测试用例中通过TestBed.overrideProvider()重新配置Mock后,重新创建组件实例。 - 更合理的做法是:在
beforeEach()中仅完成测试模块配置和组件实例创建,延迟到每个测试用例中配置Mock后再调用detectChanges(),确保ngOnInit执行时Mock已就绪。
示例调整:
beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ MyComponent ], providers: [ { provide: SomeService, useValue: jasmine.createSpyObj('SomeService', ['aMethod']) } ] }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MyComponent); component = fixture.componentInstance; compiled = fixture.nativeElement; }); it('should show mocked value', () => { const mock = TestBed.inject(SomeService) as jasmine.SpyObj<SomeService>; mock.aMethod.and.returnValue('MOCKED VALUE'); fixture.detectChanges(); // 第一次调用触发ngOnInit expect(compiled.querySelector('p').innerHTML).toContain('MOCKED VALUE'); });
4. 在每个it中调用createComponent是否是最佳实践?
是的,这是Angular测试的推荐最佳实践:
- 每个测试用例应保持独立,互不影响。在每个
it中创建组件,能确保每个测试都使用全新的组件实例和Mock服务,避免测试间的状态污染。 - 若在
beforeEach()中创建组件,多个测试用例会共享同一实例,前一个测试的修改可能干扰后续测试结果。 - 虽然会增加少量测试执行时间,但换来的测试可靠性和可维护性,在绝大多数场景下都是值得的。
内容的提问来源于stack exchange,提问作者Lucas Pottersky
相关产品推荐
相关产品推荐

