如何测试Angular17中使用toObservable()的注入式计算信号效果?
问题:Angular信号转Observable在单元测试中无法响应值变化
我有个AuthService,在构造函数里订阅了从计算信号转来的Observable,生产环境运行正常,但单元测试始终搞不定。核心问题是没法让模拟的信号变化被订阅检测到,注入上下文总不对,Jasmine间谍也不好使。
相关代码
AuthService
// 检查令牌状态操作 private expiredStatus$ = toObservable(this.permissionStore.expiredStatus) constructor() { console.log("Constructor started...."); this.expiredStatus$ .pipe( tap(() => { return console.log("Subscription Active"); }) ) .subscribe({ next: (tokenStatus) => { console.log("Token refreshed successfully, new status:", tokenStatus); // 业务逻辑中此处会调用refreshToken,测试需验证该行为 }, error: (error) => { console.error("Failed to refresh token:", error); }, }); }
PermissionStoreService
// 当令牌处于Expired状态时返回true expiredStatus: Signal<boolean> = computed(() => this.permissionsState().token_status === TokenStatus.Expired);
尝试过的方法
- 使用真实计算信号模拟:能让构造函数正常运行,但无法灵活控制值变化
TestBed.configureTestingModule({ providers: [ { provide: HttpClient, useValue: httpClientMock }, // 用于jwtHttp { provide: HttpBackend, useValue: httpBackendMock }, // 用于cleanHttp { provide: PermissionsStoreService, useValue: { // 创建真实计算信号配合effect订阅 expiredStatus: computed(() => signalMock()), }, } ] });
- 用Jasmine间谍模拟信号:测试能运行,但
TestBed.flushEffects()检测不到值变化
expiredStatusMockSignal = signal(true); expiredStatusMockComputed = computed(() => expiredStatusMockSignal()); // 定义布尔类型信号的类型 type BooleanSignal = Signal<boolean>; const permissionsStoreServiceMock = { expiredStatus: jasmine.createSpy("expiredStatus") as jasmine.Spy<() => BooleanSignal>, }; // 为间谍提供假实现 permissionsStoreServiceMock.expiredStatus.and.callFake(() => { return expiredStatusMockComputed as BooleanSignal; });
未成功的测试代码
it("should react to token expiration", () => { // 断言逻辑 const tokenRefreshCalled = spyOn(authService, "refreshToken").and.callFake(() => { console.debug("Called the faker..."); return of(TokenStatus.Valid); // 返回模拟响应 }); expiredStatusMockSignal.update(() => false); TestBed.flushEffects(); authService.expiredStatus$.subscribe((next) => console.log("Expired Status: ", next)); console.log("starting shit show...."); expiredStatusMockSignal.update(() => true); TestBed.flushEffects(); console.log("Status: ", expiredStatusMockSignal()); // 断言refreshToken被调用 expect(tokenRefreshCalled).toHaveBeenCalled(); });
解决方案
核心问题分析
问题出在构造函数初始化时就完成了toObservable转换和订阅,如果模拟信号在AuthService实例化后才替换,订阅的还是旧信号源;另外用Jasmine间谍包裹信号是错误的——Signal本身是函数,但它的可观测性依赖Angular内部机制,间谍会破坏信号的更新通知逻辑。
正确测试步骤
- 提前创建可控制的模拟信号,在TestBed配置时注入,确保AuthService构造函数使用的是这个模拟信号
- 不要用Jasmine间谍包裹Signal,直接用真实的
signal/computed模拟,保证信号更新能被toObservable捕获 - 每次变更信号值后调用
flushEffects,确保信号变化传递到Observable订阅
完整测试代码
describe('AuthService', () => { let authService: AuthService; let mockExpiredSignal: WritableSignal<boolean>; let mockPermissionStore: Partial<PermissionsStoreService>; beforeEach(() => { // 创建可写信号,用于控制expiredStatus的值 mockExpiredSignal = signal(false); // 模拟PermissionStore的expiredStatus计算信号 mockPermissionStore = { expiredStatus: computed(() => mockExpiredSignal()), }; TestBed.configureTestingModule({ providers: [ AuthService, { provide: HttpClient, useValue: jasmine.createSpyObj('HttpClient', ['get', 'post']) }, { provide: HttpBackend, useValue: jasmine.createSpyObj('HttpBackend', ['handle']) }, { provide: PermissionsStoreService, useValue: mockPermissionStore } ] }); // 实例化AuthService,此时构造函数会订阅我们的模拟信号 authService = TestBed.inject(AuthService); // 初始刷新,确保初始状态被处理 TestBed.flushEffects(); }); it('should call refreshToken when token expires', () => { // 间谍监听refreshToken方法 const refreshSpy = spyOn(authService, 'refreshToken').and.returnValue(of(TokenStatus.Valid)); // 变更信号值为true(令牌过期) mockExpiredSignal.set(true); // 刷新信号变更,确保Observable接收到新值 TestBed.flushEffects(); // 断言refreshToken被调用 expect(refreshSpy).toHaveBeenCalled(); }); });
关键注意点
- 不要用Jasmine间谍模拟Signal:Signal的内部机制依赖Angular反应式上下文,间谍会破坏更新通知逻辑,必须用真实的
signal/computed模拟 - 提前注入模拟对象:确保AuthService构造函数执行时,使用的是预先配置好的模拟信号,而非后续替换的对象
- 正确使用
flushEffects:每次变更信号值后调用,确保所有反应式更新(包括toObservable的转换)都被处理
内容的提问来源于stack exchange,提问作者MrYutz
相关产品推荐
相关产品推荐

