Ngrx Effects登录测试问题求助:报错排查与教程推荐
我看到你在测试NgRx Auth Effects时遇到了Error: Expected $.length = 0 to equal 1的问题,这是因为当前测试没有正确模拟AuthService的login方法,导致Effect无法发出预期的动作。下面我会一步步修正你的测试代码,同时解释每个部分的逻辑,最后给你一些实用的NgRx Effects测试教程要点。
首先,先修正你的测试代码(解决报错)
我们需要模拟AuthService,避免调用真实的HTTP请求,同时控制它的返回值来覆盖不同的测试场景:
import { HttpClientTestingModule } from "@angular/common/http/testing"; import { TestBed } from "@angular/core/testing"; import { provideMockActions } from "@ngrx/effects/testing"; import { Observable } from "rxjs"; import { hot, cold } from "jasmine-marbles"; import { Credentials, AuthToken } from "../../models"; import { CONFIG, Config } from "../../../../config/config.module"; import { AuthService } from "../../services"; import { AuthEffects } from "./auth.effects"; import * as fromActions from "../actions"; import { Go } from '../../../router'; const credentials: Credentials = { email: "io@io.io", password: "abc" }; const error: any = { status: 401, message: "401 Unauthorized Error" }; const token: AuthToken = { token: "abc", expiresIn: 1546356028904 }; describe("AuthEffects", () => { let effects: AuthEffects; let actions: Observable<any>; // 声明mock的AuthService let authServiceSpy: jasmine.SpyObj<AuthService>; beforeEach(() => { // 创建AuthService的spy对象,包含login方法 authServiceSpy = jasmine.createSpyObj('AuthService', ['login']); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ // 提供mock的AuthService代替真实服务 { provide: AuthService, useValue: authServiceSpy }, { provide: CONFIG, useClass: Config }, AuthEffects, provideMockActions(() => actions) ] }); effects = TestBed.inject(AuthEffects); }); // 测试登录成功场景 it('should emit LoginSuccess when login succeeds', () => { const action = new fromActions.Login({ credentials }); const completion = new fromActions.LoginSuccess({ token }); // 设置mock的login方法返回成功的Observable authServiceSpy.login.and.returnValue(cold('-a|', { a: token })); // 模拟触发Login动作 actions = hot('-a', { a: action }); // 预期延迟后发出LoginSuccess动作(和mock的login返回时序匹配) const expected = hot('--b', { b: completion }); expect(effects.login$).toBeObservable(expected); }); // 测试登录失败场景 it('should emit LoginFailure when login fails', () => { const action = new fromActions.Login({ credentials }); const completion = new fromActions.LoginFailure(error); // 设置mock的login方法返回错误的Observable authServiceSpy.login.and.returnValue(cold('-#', {}, error)); actions = hot('-a', { a: action }); const expected = hot('--b', { b: completion }); expect(effects.login$).toBeObservable(expected); }); // 测试登录成功后跳转路由的Effect it('should emit Go action when LoginSuccess is dispatched', () => { const action = new fromActions.LoginSuccess({ token }); const completion = new Go({ path: ['/admin'] }); actions = hot('-a', { a: action }); const expected = hot('-b', { b: completion }); expect(effects.loginSuccess$).toBeObservable(expected); }); });
测试逻辑解释
1. 核心概念:NgRx Effects测试的本质
NgRx Effects是监听动作流,然后触发异步操作(比如API请求),最后发出新动作的服务。测试Effects的核心是:
- 模拟输入动作流(用
provideMockActions和hot) - 控制依赖服务的返回值(比如这里的
AuthService,用spy模拟) - 验证输出动作流是否符合预期(用
toBeObservable和cold/hot)
2. 为什么之前的测试报错?
你之前的测试直接使用了真实的AuthService,它会发起真实的HTTP请求,但测试环境中没有后端服务,所以login方法返回的Observable永远不会发出值,导致login$ Effect没有任何输出,所以出现Expected $.length = 0 to equal 1的错误。
3. 关键代码解析
jasmine.createSpyObj:创建一个模拟的服务对象,我们只关心它的login方法,这样可以完全控制它的返回值,避免依赖真实后端。hot和cold(jasmine-marbles):用来模拟Observable的时序:hot('-a', {a: action}):表示在时间线的第1个时间单位发出action动作cold('-a|', {a: token}):表示在第1个时间单位发出token,然后完成(|表示完成)cold('-#', {}, error):表示在第1个时间单位抛出error(#表示错误)
- 时序匹配:因为
switchMap会等待login的Observable完成,所以我们需要让输入动作的时序和mock服务的返回时序对应,确保预期的输出动作时序正确。
实用NgRx Effects测试教程要点
1. 覆盖所有场景
每个Effect至少要测试三种场景:
- 异步操作成功时的输出动作
- 异步操作失败时的输出动作
- (可选)异常分支处理(比如空输入、超时等)
2. 始终模拟依赖服务
永远不要在Effects测试中使用真实的服务(比如调用真实API),一定要用spy或者mock对象,这样测试才是可预测、快速、不依赖外部环境的。
3. 熟练使用jasmine-marbles
jasmine-marbles是测试RxJS Observable时序的神器,掌握它的语法:
-:时间单位(默认10ms)a:发出值为a的事件|:Observable完成#:Observable抛出错误^:订阅开始的位置
4. 测试多个Effects的交互
如果你的Effects之间有依赖(比如loginSuccess$监听LoginSuccess动作),要单独测试每个Effect的独立逻辑,不要把多个Effect的测试混在一起。
5. 验证副作用
如果Effect有其他副作用(比如调用localStorage、导航),需要额外验证这些副作用是否被触发。比如在loginSuccess$的测试中,你可以模拟Router服务,验证navigate方法是否被调用。
内容的提问来源于stack exchange,提问作者Whisher

