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

Redux-Observable单元测试中Promise未解决及正确测试写法问询

解决Redux-Observable Epic测试中map操作不执行的问题

我明白你遇到的问题:你的loadPhotosToList epic在应用中运行完全正常,但在测试时,from(request)里的Promise似乎从未resolve,导致photosSuccess action无法触发——只有手动调用requestAction.subscribe()时才能看到预期结果。下面是针对这个问题的分析和解决方案:

核心原因分析

Redux-Observable的Epic是冷Observable,只有当被订阅时才会启动流中的操作。测试中出现这个问题,通常是以下几个原因之一:

  1. 测试没有正确订阅Epic的输出流,导致异步操作未被触发
  2. takeUntil逻辑在测试中意外提前终止了流
  3. 测试用的state$初始值不符合Epic执行的条件
  4. 异步操作的调度在测试中未被正确处理

具体解决方案

1. 检查takeUntil逻辑是否干扰测试流

你的Epic中使用了takeUntil(action$.pipe(filter(futureAction => futureAction.type === ACTION.FETCH_PHOTOS_REQUESTED))),这个逻辑的作用是:如果有新的FETCH_PHOTOS_REQUESTED action进来,就终止当前的请求流。

如果你的测试中不小心多次发送了这个action,或者测试框架的调度问题导致这个filter提前匹配,就会让requestAction还没执行就被终止。

临时排查方法:注释掉takeUntil部分的代码,重新运行测试,如果photosSuccess能正常触发,就说明是这个逻辑的问题。之后可以在测试中确保只发送一次FETCH_PHOTOS_REQUESTED action,或者调整takeUntil的过滤条件(比如加上filter匹配)。

2. 使用TestScheduler进行大理石测试(推荐)

Redux-Observable官方推荐用RxJS的TestScheduler进行大理石测试,它能精准控制异步流的时间和执行顺序,避免异步调度带来的问题。

示例测试代码:

import { TestScheduler } from 'rxjs/testing';
import { loadPhotosToList } from './photos';
import * as api from '../path-to-api';
import * as photosActions from '../path-to-photos-actions';
import { ACTION } from '../path-to-action-types';

describe('loadPhotosToList Epic', () => {
  let testScheduler;

  beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  it('should emit photosLoading then photosSuccess when request resolves', () => {
    testScheduler.run(({ cold, hot, expectObservable }) => {
      // Mock API请求返回resolved Promise
      jest.spyOn(api, 'fetchPhotos').mockReturnValue(Promise.resolve([{ id: 'test-photo' }]));
      
      // 模拟触发的action流
      const action$ = hot('-a', {
        a: { type: ACTION.FETCH_PHOTOS_REQUESTED, filter: 'latest', refresh: false }
      });
      
      // 模拟初始state:符合epic执行条件(idle状态、不是最后一页)
      const state$ = cold('|', {}, {
        photos: {
          latest: { loadingState: 'idle', isLastPage: false, lastLoadedPage: 0 }
        }
      });

      // 预期输出的action流
      const expected = '-b-c';
      const expectedActions = {
        b: photosActions.photosLoading('latest', false),
        c: photosActions.photosSuccess(
          [{ id: 'test-photo' }], 
          'latest', 
          1, 
          true, // 因为返回数据长度小于perPage
          false
        )
      };

      const output$ = loadPhotosToList(action$, state$);
      expectObservable(output$).toBe(expected, expectedActions);
    });
  });
});

3. 手动测试时确保订阅并等待异步完成

如果不用大理石测试,手动编写测试时要注意:必须订阅Epic的输出流,并且等待Promise完成后再断言结果。

示例代码:

import { of } from 'rxjs';
import { loadPhotosToList } from './photos';
import * as api from '../path-to-api';
import * as photosActions from '../path-to-photos-actions';
import { ACTION } from '../path-to-action-types';

it('should dispatch photosSuccess after successful request', async () => {
  // Mock API请求
  jest.spyOn(api, 'fetchPhotos').mockResolvedValue([{ id: 'test-photo' }]);
  
  // 模拟触发的action
  const action$ = of({ 
    type: ACTION.FETCH_PHOTOS_REQUESTED, 
    filter: 'latest', 
    refresh: false 
  });
  
  // 模拟符合条件的state
  const state$ = of({
    photos: {
      latest: { loadingState: 'idle', isLastPage: false, lastLoadedPage: 0 }
    }
  });

  const capturedActions = [];
  // 订阅Epic输出流
  loadPhotosToList(action$, state$).subscribe(action => {
    capturedActions.push(action);
  });

  // 等待异步Promise完成
  await new Promise(resolve => setTimeout(resolve));

  // 断言结果
  expect(capturedActions).toEqual([
    photosActions.photosLoading('latest', false),
    photosActions.photosSuccess(
      [{ id: 'test-photo' }], 
      'latest', 
      1, 
      true, 
      false
    )
  ]);
});

4. 确保测试中的state$符合执行条件

你的Epic开头的filter操作有严格的条件:

filter((a: Action) => a.type === ACTION.FETCH_PHOTOS_REQUESTED && 
  ((state(a.filter).loadingState === 'idle' && !state(a.filter).isLastPage) || a.refresh))

如果测试中state$的初始值不满足loadingState === 'idle'或者isLastPage === true,同时refresh为false,那么action会被过滤掉,后面的逻辑根本不会执行。

务必在测试中确保state$的初始值符合Epic执行的条件。

5. 调试技巧:添加tap日志定位问题

在Epic的关键位置添加tap操作,打印日志确认流的执行情况:

const requestAction = from(request)
  .pipe(
    tap(() => console.log('Promise resolved!')), // 确认Promise是否resolve
    tap(data => console.log('Received data:', data)),
    map(data => photosActions.photosSuccess(...)),
    catchError(e => of(photosActions.photosFail(e.message, a.filter))),
  );

运行测试时查看控制台日志,就能知道from(request)是否真的触发了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 07:30:24