如何在Angular组件单元测试中正确Mock NGXS的Select方法
如何在Angular组件单元测试中正确Mock NGXS的Select方法
我之前也碰到过和你一模一样的问题——测试使用NGXS的独立组件时,总是被各种依赖注入错误卡壳。你的错误本质是没正确处理所有依赖项:包括NGXS Store的mock、组件导入的子组件/模块、还有隐藏的第三方服务依赖(比如Logger/LogTarget)。下面一步步帮你搞定:
问题根源分析
你遇到的两个错误:
- NullInjectorError: Logger -> LogTarget:你的
ConfigurationManagerService或NGXS Store依赖Logger,而Logger又需要LogTarget,测试时没提供这俩的mock,Angular找不到它们。 - NGXS_EXECUTION_STRATEGY依赖错误:你尝试注入真实的
Store,但测试环境没配置完整的NGXS模块,导致内部依赖缺失。
正确的测试配置步骤
1. 准备Mock对象和测试数据
先创建所有需要mock的服务SpyObj,以及测试用的模拟数据:
- 为
ConfigurationManagerService创建SpyObj,模拟它的getTasks方法。 - 为NGXS
Store创建SpyObj,模拟select和dispatch方法(对应组件里的NGXS选择器和Action分发)。 - 为
ChangeDetectorRef创建SpyObj,模拟markForCheck方法。 - 为
Logger和LogTarget创建SpyObj,解决隐藏的依赖错误。
2. 配置TestBed(关键!)
因为是独立组件,测试时必须:
- 提供所有mock服务到
providers数组。 - 模拟组件导入的子组件/模块(比如
TextComponent、CheckboxComponent、TooltipModule),不能用真实组件,否则会引入更多依赖。
3. 编写测试用例
初始化TestBed后,验证组件的创建、生命周期方法调用、Action分发逻辑等。
完整测试代码示例
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TaskSelectionMenuComponent } from './task-selection-menu.component'; import { Store } from '@ngxs/store'; import { ConfigurationManagerService } from '../path-to-your-service-folder'; import { ChangeDetectorRef } from '@angular/core'; import { TaskSelectionActions, TaskSelectionSelectors } from '../path-to-your-ngxs-store-folder'; import { Task, TaskSelectionState, TaskState } from '../path-to-your-models-folder'; import { of } from 'rxjs'; // 如果你用ng-mocks(推荐,省得手动写mock组件) import { MockComponent, MockModule } from 'ng-mocks'; import { TextComponent } from '../path-to-text-component'; import { CheckboxComponent } from '../path-to-checkbox-component'; import { TooltipModule } from '@angular/material/tooltip'; // 假设是Material的Tooltip import { Logger } from '../path-to-logger-service'; import { LogTarget } from '../path-to-log-target-service'; // 如果你不想用ng-mocks,就手动写mock组件/模块: // @Component({ selector: 'app-text', template: '' }) // class MockTextComponent {} // @Component({ selector: 'app-checkbox', template: '' }) // class MockCheckboxComponent {} // @NgModule({}) // class MockTooltipModule {} describe('TaskSelectionMenuComponent', () => { let component: TaskSelectionMenuComponent; let fixture: ComponentFixture<TaskSelectionMenuComponent>; let storeSpy: jasmine.SpyObj<Store>; let configManagerMock: jasmine.SpyObj<ConfigurationManagerService>; let cdrSpy: jasmine.SpyObj<ChangeDetectorRef>; // 测试用模拟数据 const mockDailyTasks = { id: 'daily-task-id-123', tasks: [] }; const mockTasks: Task[] = [ { id: 'task-001', name: '早起打卡', selectionState: TaskSelectionState.Unselected }, { id: 'task-002', name: '完成日报', selectionState: TaskSelectionState.Selected } ]; beforeEach(async () => { // 创建所有SpyObj configManagerMock = jasmine.createSpyObj('ConfigurationManagerService', ['getTasks']); storeSpy = jasmine.createSpyObj('Store', ['dispatch', 'select']); cdrSpy = jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']); const loggerSpy = jasmine.createSpyObj('Logger', ['info', 'error']); const logTargetSpy = jasmine.createSpyObj('LogTarget', ['write']); // 配置mock方法的返回值 configManagerMock.getTasks.and.returnValue(Promise.resolve(mockDailyTasks)); // 模拟NGXS选择器返回的Observable数据 storeSpy.select.withArgs(TaskSelectionSelectors.props.tasks).and.returnValue(of(mockTasks)); storeSpy.select.withArgs(TaskSelectionSelectors.props.state).and.returnValue(of(TaskState.Default)); await TestBed.configureTestingModule({ providers: [ { provide: Store, useValue: storeSpy }, { provide: ConfigurationManagerService, useValue: configManagerMock }, { provide: ChangeDetectorRef, useValue: cdrSpy }, { provide: Logger, useValue: loggerSpy }, { provide: LogTarget, useValue: logTargetSpy } ], imports: [ // 用ng-mocks模拟导入的组件/模块 MockComponent(TextComponent), MockComponent(CheckboxComponent), MockModule(TooltipModule) // 如果不用ng-mocks,就用手动写的mock: // MockTextComponent, // MockCheckboxComponent, // MockTooltipModule ] }).compileComponents(); fixture = TestBed.createComponent(TaskSelectionMenuComponent); component = fixture.componentInstance; fixture.detectChanges(); // 触发ngOnInit生命周期 }); // 基础测试:组件能创建 it('should create successfully', () => { expect(component).toBeTruthy(); }); // 验证初始化逻辑:获取每日任务并分发FetchAll Action it('should fetch daily tasks and dispatch FetchAll action on init', async () => { expect(configManagerMock.getTasks).toHaveBeenCalledWith('daily'); expect(storeSpy.dispatch).toHaveBeenCalledWith(new TaskSelectionActions.FetchAll(mockDailyTasks.id)); expect(component.id).toBe(mockDailyTasks.id); }); // 验证isTaskSelected方法逻辑 it('should correctly identify selected/unselected tasks', () => { const selectedTask = mockTasks.find(t => t.selectionState === TaskSelectionState.Selected)!; const unselectedTask = mockTasks.find(t => t.selectionState === TaskSelectionState.Unselected)!; expect(component.isTaskSelected(selectedTask)).toBeTrue(); expect(component.isTaskSelected(unselectedTask)).toBeFalse(); }); // 验证updateTask方法:选中任务时分发UpdateTask Action it('should dispatch UpdateTask action when task selection is toggled', () => { const targetTask = mockTasks[0]; const expectedState = { selectionState: TaskSelectionState.Selected }; component.updateTask(targetTask, true); expect(storeSpy.dispatch).toHaveBeenCalledWith( new TaskSelectionActions.UpdateTask(mockDailyTasks.id, targetTask.id, expectedState) ); expect(cdrSpy.markForCheck).toHaveBeenCalled(); }); // 验证updateTask边界情况:id未设置时不分发Action it('should NOT dispatch UpdateTask if component id is undefined', () => { component.id = undefined; const targetTask = mockTasks[0]; component.updateTask(targetTask, true); expect(storeSpy.dispatch).not.toHaveBeenCalled(); expect(cdrSpy.markForCheck).toHaveBeenCalled(); }); });
关键注意事项
- 独立组件的导入项必须全mock:因为独立组件不依赖NgModule,测试时必须模拟所有导入的组件/模块,否则会引入它们的依赖链,导致更多注入错误。
- NGXS选择器的mock:组件里用
select(TaskSelectionSelectors.props.tasks)创建Observable,所以我们让storeSpy.select返回预定义的Observable数据,确保组件能拿到正确的任务列表。 - 隐藏依赖的处理:像Logger/LogTarget这种间接依赖,一定要找到并mock,否则Angular的注入器会报错。
内容来源于stack exchange
相关产品推荐
相关产品推荐

