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

如何在Angular组件单元测试中正确Mock NGXS的Select方法

如何在Angular组件单元测试中正确Mock NGXS的Select方法

我之前也碰到过和你一模一样的问题——测试使用NGXS的独立组件时,总是被各种依赖注入错误卡壳。你的错误本质是没正确处理所有依赖项:包括NGXS Store的mock、组件导入的子组件/模块、还有隐藏的第三方服务依赖(比如Logger/LogTarget)。下面一步步帮你搞定:

问题根源分析

你遇到的两个错误:

  1. NullInjectorError: Logger -> LogTarget:你的ConfigurationManagerService或NGXS Store依赖Logger,而Logger又需要LogTarget,测试时没提供这俩的mock,Angular找不到它们。
  2. 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();
  });
});

关键注意事项

  1. 独立组件的导入项必须全mock:因为独立组件不依赖NgModule,测试时必须模拟所有导入的组件/模块,否则会引入它们的依赖链,导致更多注入错误。
  2. NGXS选择器的mock:组件里用select(TaskSelectionSelectors.props.tasks)创建Observable,所以我们让storeSpy.select返回预定义的Observable数据,确保组件能拿到正确的任务列表。
  3. 隐藏依赖的处理:像Logger/LogTarget这种间接依赖,一定要找到并mock,否则Angular的注入器会报错。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:30:28