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

如何用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方法,所以不能直接监听该方法的调用。可以通过两种方式验证目标逻辑:

  1. 监听Signal转为Observable后的变化,确认无更新时不会触发新的回调;
  2. 直接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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 12:50:11