如何用Jest测试Angular 18中的Signal?迁移测试遇阻求助
问题描述
我正尝试从RxJS BehaviorSubject迁移至Angular的Signal。原本测试BehaviorSubject更新的用例,迁移到Signal后Jest测试出问题了。
服务代码
import { Injectable, signal } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { BehaviorSubject } from 'rxjs'; export interface SignalState { foo: string; bar: boolean | undefined; } @Injectable({ providedIn: 'root', }) export class SignalService { // private signalState$ = new BehaviorSubject<SignalState>({ foo: '', bar: undefined }); //signalState bevor private signaltState = signal<SignalState>({ foo: '', bar: undefined }); private signalState$ = toObservable(this.signaltState); public getValue = this.signaltState.asReadonly(); public getSignalState() { return this.signalState$; } public updateFoo(foo: string) { this.updateSignal({ foo }); } private isChanged(currentState: SignalState, updatedState: SignalState) { return JSON.stringify(currentState) !== JSON.stringify(updatedState); } private updateSignal(newState: Partial<SignalState>) { const currentState = this.getValue(); const updatedState: SignalState = { ...currentState, ...newState }; if (this.isChanged(currentState, updatedState)) { this.signaltState.set(updatedState); } } }
原测试代码(针对BehaviorSubject)
import { SpectatorService } from '@ngneat/spectator'; import { createServiceFactory } from '@ngneat/spectator/jest'; import { SignalService, SignalState } from './lead-request-state.service'; describe('SignalService', () => { let spectator: SpectatorService<SignalService>; let service: SignalService; const currentState: SignalState = { foo: 'someString', bar: false }; const createService = createServiceFactory({ service: SignalService, }); beforeEach(() => { spectator = createService(); service = spectator.inject(SignalService); service.getSignalState().next(currentState); }); it('should created service', () => { expect(service).toBeTruthy(); }); it('should not update state if value is not changed', () => { const nextSpy = jest.spyOn(service.getSignalState(), 'next'); service.updateFoo(currentState.foo); expect(nextSpy).not.toHaveBeenCalled(); }); });
我要验证的核心逻辑是:当状态未变化时,this.signaltState.set(updatedState);不会被调用,从而不触发变更检测。请问针对Signal该怎么实现这个测试?
解决方案
核心思路
Signal没有BehaviorSubject那样的next方法,所以不能直接监听该方法的调用。可以通过两种方式验证目标逻辑:
- 监听Signal转为Observable后的变化,确认无更新时不会触发新的回调;
- 直接Spy Signal的
set方法,验证状态未变时该方法不被调用。
方法一:通过Observable监听变化(贴近实际业务场景)
利用toObservable将Signal转为Observable,订阅后用mock函数记录回调触发次数:
import { SpectatorService } from '@ngneat/spectator'; import { createServiceFactory } from '@ngneat/spectator/jest'; import { SignalService, SignalState } from './lead-request-state.service'; import { take } from 'rxjs'; describe('SignalService', () => { let spectator: SpectatorService<SignalService>; let service: SignalService; const currentState: SignalState = { foo: 'someString', bar: false }; const createService = createServiceFactory({ service: SignalService, }); beforeEach(() => { spectator = createService(); service = spectator.inject(SignalService); // 初始化状态:直接操作私有成员(测试场景下允许) (service as any).signaltState.set(currentState); }); it('should not trigger state change when value is unchanged', (done) => { const changeSpy = jest.fn(); // 订阅Observable,先获取一次初始值 service.getSignalState().pipe(take(1)).subscribe(changeSpy); // 调用updateFoo传入相同值 service.updateFoo(currentState.foo); // 延迟验证确保异步逻辑完成 setTimeout(() => { // 仅初始订阅会触发一次,后续无更新,所以spy只被调用1次 expect(changeSpy).toHaveBeenCalledTimes(1); done(); }, 0); }); });
方法二:Spy Signal的set方法(直接验证核心操作)
通过类型断言访问私有Signal成员,Spy其set方法来验证是否被调用:
it('should not call set when state is unchanged', () => { // 获取私有Signal并Spy其set方法 const signalSetSpy = jest.spyOn((service as any).signaltState, 'set'); // 调用updateFoo传入相同值 service.updateFoo(currentState.foo); // 验证set方法未被调用 expect(signalSetSpy).not.toHaveBeenCalled(); });
额外优化:初始化状态的合规方式
如果不想在测试中直接操作私有成员,可以给服务添加一个公开的初始化方法:
// 在SignalService中新增 public initializeState(state: Partial<SignalState>) { this.updateSignal(state); }
之后在测试的beforeEach中用service.initializeState(currentState);完成状态初始化,避免直接访问私有成员。
内容的提问来源于stack exchange,提问作者Ruth
相关产品推荐
相关产品推荐

