Ionic 3中ngrx/effects测试:TestBed配置与动作流转验证咨询
完全理解你找不到合适教程的挫败感——NgRx版本迭代快,老示例经常失效,尤其是结合Ionic的场景。下面针对你的两个核心问题给出清晰的入门指引:
一、TestBed的正确配置:必须隔离所有外部依赖
Effects的核心是处理动作流并与服务交互,所以测试时必须用Mock替换所有真实依赖,否则会引入外部副作用(比如真实API请求、存储操作),导致测试不稳定。
你的当前配置问题修正方向:
替换所有真实服务为Mock
你当前的TestBed里直接引入了ToastService和ToastController的真实实现,这会触发真实的Toast弹窗,完全没必要。需要为这些服务创建极简Mock:// 示例:ToastService的Mock类 export class ToastServiceMock { couldNotUpdateObject() {} // 空实现,只用来验证是否被调用 }同理,
LogService也需要Mock,避免真实的日志输出干扰测试。修正Mock服务的方法签名
你提供的MyObjectsServiceMock里的方法是loadMyObjects(),但真实服务里的方法是loadObjects()——方法名不匹配会导致Effect调用时出错,必须保持签名完全一致。确保provideMockActions正确注入
provideMockActions(() => actions)是NgRx提供的专门用来模拟动作流的工具,你需要把actions定义为一个可观察对象(比如Subject或ReplaySubject),这样才能动态派发测试用的动作。
标准TestBed配置模板:
beforeEach(() => { TestBed.configureTestingModule({ providers: [ MyEffects, // 模拟动作流 provideMockActions(() => actions), // 所有依赖全部用Mock替换 { provide: MyObjectService, useClass: MyObjectServiceMock }, { provide: LogService, useClass: LogServiceMock }, { provide: ToastService, useClass: ToastServiceMock }, { provide: Store, useClass: StoreMock }, { provide: Storage, useClass: StorageMock }, // Ionic的控制器也要Mock,比如ToastController { provide: ToastController, useClass: ToastControllerMock } ] }); myObjectEffects = TestBed.inject(MyEffects); // 获取Mock实例,后续可以设置返回值或验证调用 myObjectServiceMock = TestBed.inject(MyObjectService) as jasmine.SpyObj<MyObjectService>; });
二、测试动作流转的两种有效方式
针对你要测试的「派发LoadAllAction → Effect处理 → 派发LoadAllCompleteAction」场景,有两种常用的测试方法,根据场景选择:
方法1:大理石测试(Jasmine Marbles)—— 适合异步流的时序验证
大理石测试是NgRx官方推荐的方式,能清晰描述动作流的时间线,适合复杂的异步场景。
你的当前测试错误修正:
- 你之前的测试里用了
{a: InitMyObjectsCompleteAction},这是传递了类,而不是动作实例——必须传递new LoadAllAction()这样的实例。 - 要测试
loadAllAction$,需要模拟派发LoadAllAction,而不是InitMyObjectsCompleteAction。
测试示例框架:
it('should dispatch LoadAllCompleteAction when LoadAllAction is triggered', () => { // 1. 准备测试数据 const dummyObjects: IMyObject[] = [/* 你的测试数据 */]; // 2. 配置Mock服务返回模拟数据 spyOn(myObjectServiceMock, 'loadObjects').and.returnValue(cold('-b', { b: dummyObjects })); // 3. 模拟动作流:在时间点'-a'派发LoadAllAction actions = hot('-a', { a: new LoadAllAction() }); // 4. 预期的结果流:在动作派发后延迟一点,返回LoadAllCompleteAction const expected = cold('--c', { c: new LoadAllCompleteAction(dummyObjects) }); // 5. 验证Effect的输出是否符合预期 expect(myObjectEffects.loadAllAction$).toBeObservable(expected); });
方法2:订阅测试—— 适合简单场景的直接验证
如果你的场景逻辑简单,不需要验证时序,直接订阅Effect的输出流即可。
你的当前测试错误修正:
- 你没有处理异步逻辑,测试会在订阅回调执行前结束,需要用
done回调或async/await处理。 - 要确保Mock服务的
loadObjects方法返回正确的Observable。
测试示例框架:
it('should emit LoadAllCompleteAction after LoadAllAction', (done) => { // 1. 准备测试数据 const dummyObjects: IMyObject[] = [/* 你的测试数据 */]; // 2. 配置Mock服务返回模拟数据 spyOn(myObjectServiceMock, 'loadObjects').and.returnValue(of(dummyObjects)); // 3. 创建动作流Subject actions = new ReplaySubject(); // 4. 订阅Effect的输出,验证结果 myObjectEffects.loadAllAction$.subscribe(result => { expect(result).toEqual(new LoadAllCompleteAction(dummyObjects)); done(); // 通知测试异步逻辑完成 }); // 5. 派发测试动作 actions.next(new LoadAllAction()); });
额外实用提示
- 验证依赖方法的调用:比如测试错误场景时,你可以用
spyOn(toastServiceMock, 'couldNotUpdateObject'),然后验证它是否被调用。 - 测试错误分支:模拟服务抛出错误,验证Effect是否派发
MyObjectsErrorAction,同时验证LogService.error是否被调用。 - 保持测试单一职责:每个测试只验证一个逻辑分支,比如一个测试验证成功场景,另一个验证错误场景。
内容的提问来源于stack exchange,提问作者TobiDevloft

