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

如何测试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);

尝试过的方法

  1. 使用真实计算信号模拟:能让构造函数正常运行,但无法灵活控制值变化
TestBed.configureTestingModule({
    providers: [
        { provide: HttpClient, useValue: httpClientMock }, // 用于jwtHttp
        { provide: HttpBackend, useValue: httpBackendMock }, // 用于cleanHttp
        {
            provide: PermissionsStoreService,
            useValue: {
                // 创建真实计算信号配合effect订阅
                expiredStatus: computed(() => signalMock()),
            },
        }
    ]
});
  1. 用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内部机制,间谍会破坏信号的更新通知逻辑。

正确测试步骤

  1. 提前创建可控制的模拟信号,在TestBed配置时注入,确保AuthService构造函数使用的是这个模拟信号
  2. 不要用Jasmine间谍包裹Signal,直接用真实的signal/computed模拟,保证信号更新能被toObservable捕获
  3. 每次变更信号值后调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 03:21:03