Angular组件NgRx Store选择器单元测试问题:覆盖率未达标及订阅断言错误修复咨询
解决NgRx组件测试的覆盖率与报错问题
让我们一步步拆解并解决你的问题:
一、关于覆盖率与store.select().subscribe的疑问
首先明确:不需要在测试里手动调用store.select().subscribe。你的组件代码里已经在ngOnInit中完成了订阅,测试的核心是确保这个订阅能正确接收到Mock Store的值,从而覆盖到this.isLoading = data这行代码。
你最初的覆盖率未达标,本质是因为Mock Store的选择器没有和组件里的选择器匹配上:
- 组件用的是匿名选择器:
select((state: AppState) => state.spinnerState.isLoading) - 但测试里Mock的是字符串标识的
'isLoading'选择器,两者无法匹配,导致组件的订阅回调根本没触发,自然没覆盖到代码。
解决思路是把匿名选择器提取成命名选择器,这样测试和组件能共用同一个选择器,避免匹配问题。比如在你的SpinnerState文件里定义:
// spinner.state.ts import { createFeatureSelector, createSelector } from '@ngrx/store'; export const selectSpinnerState = createFeatureSelector<SpinnerState>('spinnerState'); export const selectIsLoading = createSelector( selectSpinnerState, (state) => state.isLoading );
然后组件里改成:
// spinner.component.ts ngOnInit(): void { this.store$.pipe(select(SpinnerState.selectIsLoading)).subscribe((data) => { this.isLoading = data; }); }
二、修复expect(mockData.isLoading).toBe(false)的报错
你修改后的测试报错,核心原因是Mock Store的返回值类型不匹配:
- 你用
store.overrideSelector(SpinnerState.selectSpinnerState, false),把返回值设成了布尔值false - 但
selectSpinnerState这个选择器应该返回的是整个SpinnerState对象(比如{isLoading: boolean}),所以当你订阅它时,mockData是false,访问false.isLoading自然会抛出“无法读取undefined的isLoading属性”的错误。
正确的测试代码修改方案
下面是调整后的完整测试代码,同时解决覆盖率和报错问题:
import { SpinnerComponent } from './spinner.component'; import { AppState } from '../../../core/store/app.state'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { SpinnerState, selectIsLoading } from '../store/spinner.state'; // 引入命名选择器 import { fakeAsync, tick } from '@angular/core/testing'; fdescribe('SpinnerComponent', () => { let component: SpinnerComponent; let fixture: ComponentFixture<SpinnerComponent>; let store: MockStore<AppState>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [SpinnerComponent], providers: [provideMockStore()], // 先不设置初始selectors,后续用overrideSelector动态调整 }).compileComponents(); }); beforeEach(() => { store = TestBed.inject(MockStore); // 初始化Mock选择器,返回默认的isLoading值 store.overrideSelector(selectIsLoading, false); fixture = TestBed.createComponent(SpinnerComponent); component = fixture.componentInstance; }); it('should create', () => { expect(component).toBeTruthy(); }); describe('when ngOnInit() is called', () => { it('should set isLoading to false initially', fakeAsync(() => { component.ngOnInit(); // 触发Mock Store的数据流更新 store.refreshState(); tick(); // 模拟异步订阅完成 expect(component.isLoading).toBe(false); })); it('should update isLoading when store state changes', fakeAsync(() => { component.ngOnInit(); // 修改Mock选择器的返回值 store.overrideSelector(selectIsLoading, true); store.refreshState(); tick(); expect(component.isLoading).toBe(true); })); }); });
关键修改点说明
- 用命名选择器替代匿名选择器:确保组件和测试使用同一个选择器,避免匹配失败
- 正确设置Mock返回值:如果是
selectIsLoading选择器(直接返回布尔值),就传入布尔值;如果是selectSpinnerState(返回整个状态对象),就传入{isLoading: false} - 用
fakeAsync/tick处理异步订阅:确保订阅的回调函数执行完成后再做断言 - 调用
store.refreshState():触发Mock Store的数据流更新,让组件的订阅能接收到新值
内容的提问来源于stack exchange,提问作者arj
相关产品推荐
相关产品推荐

