Angular单元测试为何创建两个模拟子组件实例?如何解决
问题根本原因
测试环境出现两个Mock/真实子组件实例的核心原因是TestBed测试模块的组件声明冲突,和Angular框架本身的渲染逻辑无关,具体触发场景如下:
- 测试模块的
declarations数组中同时声明了真实ChildComponent和自定义MockChildComponent,两个组件如果选择器相同,Angular编译模板时会为同一个子组件标签匹配到两个组件类,渲染时分别生成独立实例。 - 配置测试模块时滥用
CUSTOM_ELEMENTS_SCHEMA或NO_ERRORS_SCHEMA跳过组件标签校验,掩盖了Mock组件选择器和真实组件不一致、子组件未正确声明的问题,导致模板解析时同时生成未被父组件识别的游离组件实例。 - 子组件实例查询时令牌不匹配:父组件
@ViewChild(ChildComponent)的查询令牌是真实子组件类,但测试代码通过debugElement.query(By.directive(MockChildComponent))查询Mock类实例,二者本身就是不同声明生成的两个独立对象,自然无法共享事件流。 - 重复做子组件替换:既在
declarations中声明Mock子组件,又调用TestBed.overrideComponent额外替换父组件中的子组件引用,导致重复生成实例。
生产环境下业务模块只会对ChildComponent做一次声明,选择器和查询令牌唯一匹配,因此只会生成一个子组件实例,不会出现交互异常。
正确TestBed配置方案
按照以下步骤配置即可保证全链路只生成一个子组件实例,@ViewChild引用和debugElement查询结果完全一致:
- 第一步:编写和真实组件接口对齐的Mock子组件,保证选择器、输出属性名和真实组件完全一致,不需要实现子组件内部逻辑:
@Component({ // selector必须和真实ChildComponent完全一致 selector: 'app-child', template: '' }) class MockChildComponent { // 输出属性名必须和真实组件保持一致,这里用ReplaySubject模拟原EventEmitter的事件流即可 selectionChange = new ReplaySubject<string>(1); }
- 第二步:配置TestBed时移除所有真实
ChildComponent的声明,不要导入包含真实ChildComponent声明的业务模块,仅声明父组件和Mock子组件,非必要不要添加schema跳过校验:
beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ ParentComponent, MockChildComponent // 仅声明Mock子组件,不要加入真实ChildComponent ], imports: [ // 仅导入父组件运行必需的公共依赖(如Material模块、CommonModule等),不要导入声明了真实ChildComponent的业务Feature模块 MatSelectModule, BrowserAnimationsModule ] // 不要配置schemas: [CUSTOM_ELEMENTS_SCHEMA/NO_ERRORS_SCHEMA],如果模板报组件未声明错误,优先补全对应声明而非跳过校验 }).compileComponents(); });
- 第三步:测试用例中查询子组件实例时,使用和父组件
@ViewChild一致的查询令牌,确保拿到的是被父组件绑定的同一个实例:
it('should receive selected value from child component', () => { const fixture = TestBed.createComponent(ParentComponent); const parentComp = fixture.componentInstance; // 首次变更检测会触发ngAfterViewInit,完成父组件对@ViewChild实例的事件绑定 fixture.detectChanges(); // 查询子组件时传入MockChildComponent作为指令令牌,不要传真实ChildComponent类 const childDebugEl = fixture.debugElement.query(By.directive(MockChildComponent)); const childComp = childDebugEl.componentInstance as MockChildComponent; // 手动触发选择变更事件 childComp.selectionChange.next('selected-test-value'); fixture.detectChanges(); // 断言父组件正确接收值 expect(parentComp.currentSelection).toBe('selected-test-value'); });
补充说明:如果使用ng-mocks等自动Mock工具,不要同时手动编写自定义Mock子组件,避免重复声明冲突。
内容的提问来源于stack exchange,提问作者Gary Sheppard
相关产品推荐
相关产品推荐

