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

如何测试Angular 19的rxResource?调用未触发问题排查

Angular 19 rxResource 单元测试:请求未触发的问题及解决方案

问题描述

我正在对使用 Angular 19 新引入的 rxResource 工具的服务进行单元测试,目标服务代码如下:

import { computed, inject, Injectable, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { Observable, of } from 'rxjs';
import { RESULTS_REPOSITORY } from '../some-tokens';

@Injectable()
export class CommonResultsDataStore {
  readonly query = signal<string | null>(null);

  private readonly repositoryService = inject<{ search: (v: string) => Observable<object[]>; }>(RESULTS_REPOSITORY);
  private readonly resource = rxResource({
    request: () => this.query(),
    loader: ({ request }) => request ? this.repositoryService.search(request) : of(null),
  });

  data = computed(() => this.resource.value() ?? []);
}

编写的 Jest 测试套件如下:

import { TestBed } from '@angular/core/testing';
import { Subject } from 'rxjs';
import { RESULTS_REPOSITORY } from '../some-tokens';

import { CommonResultsDataStore } from '.';

describe('CommonResultsDataStore', () => {
  const searchSubject = new Subject<object[]>();
  const mockRepository = {
    search: jest.fn((_v: string) => searchSubject.asObservable()),
  };

  let service: CommonResultsDataStore;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: RESULTS_REPOSITORY, useValue: mockRepository },
        CommonResultsDataStore,
      ],
    });
    service = TestBed.inject(CommonResultsDataStore);
  });

  describe('initial state', () => {
    it('should return empty results', () => {
      expect(service.data()).toStrictEqual([]);
      expect(mockRepository.search).not.toHaveBeenCalled();
    });
  });

  describe('with remote query', () => {
    beforeEach(() => {
      service.query.set('toto');
    });

    it('should return empty results', () => {
      expect(service.data()).toStrictEqual([]);
      expect(mockRepository.search).toHaveBeenCalledTimes(1); // ❌ 未触发调用
      expect(mockRepository.search).toHaveBeenCalledWith('toto');
    });

    it('should return results', () => {
      // When
      const flushedData = [
        { id: 1 },
        { id: 2 },
        { id: 3 },
        { id: 4 },
        { id: 5 },
      ];
      searchSubject.next(flushedData);

      // Then
      expect(service.data()).toStrictEqual([]);
      expect(mockRepository.search).toHaveBeenCalledTimes(1); // ❌ 未触发调用
      expect(mockRepository.search).toHaveBeenCalledWith('toto');
    });
  });
});

问题:设置 query 信号后,预期会调用 repository 服务,但实际未触发任何调用。尝试过使用 toObservable 工具及在设置信号前手动订阅,均未成功。

问题原因

  1. rxResource 的异步调度特性:rxResource 内部使用 RxJS 的异步调度器(默认是 asyncScheduler)处理请求触发逻辑。当 request 信号(即 query)变化时,loader 函数的执行是异步的,测试中同步执行断言时,异步操作尚未完成。
  2. 断言时机过早:当前测试未使用 Angular 提供的异步测试工具控制流程,导致在 loader 函数还未被调用时就执行了断言。

推荐测试方式

使用 Angular 测试工具 fakeAsync 和 tick() 模拟异步流程,确保在断言前等待所有异步操作完成。同时调整测试结构,避免测试间的状态干扰。

修改后的测试代码

import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { Subject } from 'rxjs';
import { RESULTS_REPOSITORY } from '../some-tokens';

import { CommonResultsDataStore } from '.';

describe('CommonResultsDataStore', () => {
  const searchSubject = new Subject<object[]>();
  const mockRepository = {
    search: jest.fn((_v: string) => searchSubject.asObservable()),
  };

  let service: CommonResultsDataStore;

  beforeEach(() => {
    // 重置 mock 和 subject 状态,避免测试间污染
    jest.clearAllMocks();
    searchSubject.next([]);
    
    TestBed.configureTestingModule({
      providers: [
        { provide: RESULTS_REPOSITORY, useValue: mockRepository },
        CommonResultsDataStore,
      ],
    });
    service = TestBed.inject(CommonResultsDataStore);
  });

  describe('initial state', () => {
    it('should return empty results', () => {
      expect(service.data()).toStrictEqual([]);
      expect(mockRepository.search).not.toHaveBeenCalled();
    });
  });

  describe('with remote query', () => {
    it('should trigger search and return empty results initially', fakeAsync(() => {
      // When
      service.query.set('toto');
      tick(); // 推进时间,让 rxResource 完成异步请求调度

      // Then
      expect(mockRepository.search).toHaveBeenCalledTimes(1);
      expect(mockRepository.search).toHaveBeenCalledWith('toto');
      expect(service.data()).toStrictEqual([]);
    }));

    it('should return results when search emits data', fakeAsync(() => {
      // Given
      service.query.set('toto');
      tick(); // 等待搜索请求触发

      // When
      const flushedData = [
        { id: 1 },
        { id: 2 },
        { id: 3 },
        { id: 4 },
        { id: 5 },
      ];
      searchSubject.next(flushedData);
      tick(); // 等待 rxResource 处理返回的数据

      // Then
      expect(service.data()).toStrictEqual(flushedData);
      expect(mockRepository.search).toHaveBeenCalledTimes(1);
    }));
  });
});

关键修改说明

  • 使用 fakeAsync 包裹测试函数:创建虚拟异步测试环境,允许手动控制时间推进。
  • 调用 tick():模拟时间流逝,触发所有待处理的异步操作(包括 rxResource 的 loader 执行、信号更新)。
  • 重置测试状态:在 beforeEach 中清除 mock 调用记录并重置 subject,确保每个测试的初始状态独立。
  • 调整测试结构:将 query.set 移至测试函数内部,替代 beforeEach,避免测试间的状态干扰。

内容的提问来源于stack exchange,提问作者fservantdev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 11:15:15