Redux-Observable单元测试中Promise未解决及正确测试写法问询
map操作不执行的问题 我明白你遇到的问题:你的loadPhotosToList epic在应用中运行完全正常,但在测试时,from(request)里的Promise似乎从未resolve,导致photosSuccess action无法触发——只有手动调用requestAction.subscribe()时才能看到预期结果。下面是针对这个问题的分析和解决方案:
核心原因分析
Redux-Observable的Epic是冷Observable,只有当被订阅时才会启动流中的操作。测试中出现这个问题,通常是以下几个原因之一:
- 测试没有正确订阅Epic的输出流,导致异步操作未被触发
takeUntil逻辑在测试中意外提前终止了流- 测试用的
state$初始值不符合Epic执行的条件 - 异步操作的调度在测试中未被正确处理
具体解决方案
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

