首页 > 解决方案 > 在 Jest 测试中订阅史诗时出错,并显示消息“您在预期流的位置提供了无效对象。”

问题描述

我对 redux-observable 很陌生,所以我不确定这是一个真正的问题还是一个愚蠢的错误。

我正在尝试将测试添加到我为 React 应用程序编写的史诗中,但我无法测试它们。这就是例子。

我的示例史诗:

const example = action$ =>
     action$.pipe(
        ofType('my_type'),
        mergeMap(() => {
            return { type: 'result_type'}
        })
    ); 

我试图以最简单的方式对其进行测试:

it('simple test', done => {
    const action$ = ActionsObservable.of({ type: 'my_type'});
    const state$ = null;

    return myEpics.example(action$, state$)
        .subscribe(actions => {
            expect(actions).toEqual({ type: 'result_type'});
            done();
        })

});

使用此代码,我收到以下错误:

TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.

      12 | 
      13 |     return myEpics.example(action$, state$)
    > 14 |         .subscribe(actions => {
         |          ^


我正在使用 redux-observable 1.2.0,rxjs 6.5.3

我错过了什么?我看到了很多这种类型的例子,它们很有效。

编辑

我真正在做的(除了虚拟示例)是替换基于 redux-thunk promise 的 api 中间件(对 ReST 端点进行许多异步调用)。我能够让它工作,但关于测试我有点困惑。我看到了弹珠图方法,以及订阅史诗的方法增加了期望,例如对 api 调用和结果操作。我使用的是后者,但是,例如,对于返回多个动作的史诗,我只得到订阅中的第一个。

一个“复杂”的例子:

export const handleLogIn = (action$, state$) =>
    action$.pipe(
        ofType(authTypes.LOG_IN),
        withLatestFrom(state$),
        mergeMap(([{ payload: credentials}, { router }]) =>
            from(ApiService.logIn(credentials))
                .pipe(
                    mergeMap(authToken => of(authActions.set_auth_token(authToken), navigationActions.goTo(router.location.state ? router.location.state.pathname : baseRoutes.START),
                    catchError(message => of(authActions.set_auth_error(message)))
                )
        )
    );

测试

it('should handle LOG_IN navigating to base path', done => {
    const token = 'aToken';
    const credentials = { username: 'user', password: 'password' };
    const action$ = ActionsObservable.of(authActions.log_in(credentials));
    const state$ = new StateObservable(new Subject(), { router: { location: { state: null }}});

    spyOn(ApiService, 'logIn').and.returnValue(Promise.resolve(token));

    authEpics.handleLogIn(action$, state$)
        .subscribe(actions => {
            expect(ApiService.logIn).toHaveBeenCalledWith(credentials);
            expect(actions).toEqual([
                authActions.set_auth_token(token),
                navigationActions.go_to(baseRoutes.START)
            ]);
            done();
        });

});

测试失败,因为只有 authActions.set_auth_token 是返回的唯一操作。我错过了什么吗?

标签: rxjsjestjsredux-observable

解决方案


欢迎!

问题是 RxJS 错误。mergeMap是一个基本上是一个map()加号的运算符mergeAll()。也就是说,在您提供的回调中,您应该返回类似流的东西。通常这是另一个 Observable,但它也可以是 Promise、Array 或 Iterable。

mergeMap 是一对多运算符之一,它与 concatMap、switchMap 和 exhaustMap 之间的区别在于,它们每个都有不同的策略来处理如果在您投影返回的内部流之前发出新的源值会发生什么to 还没有结束。

在 mergeMap 的情况下,如果在内部流完成之前发出另一个源值,它将再次调用您的回调(函数式编程术语中的“项目”)并订阅它,同时将任何结果值与先前投影的内部流合并。


在您的情况下,您在 mergeMap 中返回一个普通的旧 JavaScript 对象,它不是流式的(Observable、Promise、Array 或 Iterable)。如果实际上您只想将一个动作 1:1 映射到另一个动作,则可以使用 map:

import { map } from 'rxjs/operators';

const example = action$ =>
  action$.pipe(
    ofType('my_type'),
    map(() => {
      return { type: 'result_type' };
    })
); 

但是请注意,这通常不是惯用的 Redux(一些罕见的例外),原始操作可能是您的减速器应该处理的。但是您可能只是为了“hello world”风格的东西而这样做,这非常酷。

也就是说,仍然可以从 mergeMap 内部同步返回一个(或多个)值。您可以使用of()

import { of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

const example = action$ =>
  action$.pipe(
    ofType('my_type'),
    map(() => {
      return of({ type: 'result_type' });
      // or more than one
      // return of({ type: 'first'}, { type: 'second' }, ...etc);
    })
); 

请记住:redux-observable 是一个用于执行 RxJS + Redux 的小型辅助库。这意味着你真的会学习 RxJS 和 Redux,因为除了“Epic”之外,关于 redux-observable 本身几乎没有什么可学习的,它是一种构建 RxJS 代码的模式和ofType()糖之上的运算符filter()


如果你感到困惑——你并不孤单——最好在Stackblitz中玩耍和/或查看一些更多涉及此的 RxJS 教程。虽然很难理解,但一旦你理解了,RxJS 解锁的功能对于复杂的异步代码就会变得更加明显。


推荐阅读