集成测试 redux-observable 发出多个 ajax 具有去抖动的请求

Integration test redux-observable that makes multiple ajax requests with debounce

我有一个 redux-observable 史诗轮询端点,获取进度更新,直到进度为 100%。轮询间隔是使用 debounceTime 实现的,如下所示:

function myEpic(action$, store, dependencies) {
  return action$.ofType('PROCESSING')
    .do(action => console.log(`RECEIVED ACTION: ${JSON.stringify(action)}`))
    .debounceTime(1000, dependencies.scheduler)
    .mergeMap(action => (
      dependencies.ajax({ url: action.checkUrl })
        .map((resp) => {
          if (parseInt(resp.progress, 10) === 100) {
            return { type: 'SUCCESS' };
          }
          return { checkUrl: resp.check_url, progress: resp.progress, type: 'PROCESSING' };
        })));
}

这很好用,但我想编写一个集成测试来测试当进度为 25%、50%、100% 时商店的状态。

在我的集成测试中,我可以将 dependencies.scheduler 设置为 new VirtualTimeScheduler()

这就是我目前正在尝试的方式(使用玩笑):

  describe('my integration test', () => {

    const scheduler = new VirtualTimeScheduler();

    beforeEach(() => {

      // Fake ajax responses
      const ajax = (request) => {

        console.log(`FAKING REQUEST FOR URL: ${request.url}`);

        if (request.url === '/check_url_1') {
          return Observable.of({ progress: 25, check_url: '/check_url_2' });
        } else if (request.url === '/check_url_2') {
          return Observable.of({ progress: 50, check_url: '/check_url_3' });
        } else if (request.url === '/check_url_3') {
          return Observable.of({ progress: 100 });
        }
        return null;
      };
      store = configureStore(defaultState, { ajax, scheduler });
    });

    it('should update the store properly after each call', () => {
      store.dispatch({ checkUrl: '/check_url_1', progress: 0, type: 'PROCESSING' });

      scheduler.flush();
      console.log('CHECK CORRECT STATE FOR PROGRESS 25');

      scheduler.flush();
      console.log('CHECK CORRECT STATE FOR PROGRESS 50');

      scheduler.flush();
      console.log('CHECK CORRECT STATE FOR PROGRESS 100');
    });
  });

我的预期输出是:

RECEIVED ACTION: {"checkUrl":"/check_url_1","progress":0,"type":"PROCESSING"}
FAKING REQUEST FOR URL: /check_url_1
CHECK CORRECT STATE FOR PROGRESS 25
RECEIVED ACTION: {"checkUrl":"/check_url_2","progress":25,"type":"PROCESSING"}
FAKING REQUEST FOR URL: /check_url_2
CHECK CORRECT STATE FOR PROGRESS 50
RECEIVED ACTION: {"checkUrl":"/check_url_3","progress":50,"type":"PROCESSING"}
# CHECK CORRECT STATE FOR PROGRESS 100

但我得到的输出是

RECEIVED ACTION: {"checkUrl":"/check_url_1","progress":0,"type":"PROCESSING","errors":null}
FAKING REQUEST FOR URL: /check_url_1
RECEIVED ACTION: {"checkUrl":"/check_url_2","progress":25,"type":"PROCESSING","errors":null}
CHECK CORRECT STATE FOR PROGRESS 25%
CHECK CORRECT STATE FOR PROGRESS 50%
CHECK CORRECT STATE FOR PROGRESS 100%

此时测试结束。我正在配置商店,以便我可以像推荐的那样模拟 ajax 请求和用于 debounceTime 的调度程序 here

所以我的问题是如何在三个 ajax 请求中的每一个请求之后测试我的商店的状态?

有趣的是,我试过你的代码并且相当有信心你刚刚在 debounceTime 运算符中发现了一个错误,它导致明显吞噬了预定的去抖动。坏消息是,即使该错误已修复,您的代码仍然无法按顺序执行您要查找的内容。

请耐心等待,因为狗屎即将成为现实:

  • Epic 接收操作 PROCESSING 并安排去抖动,让执行执行您的测试
  • 您的测试调用 scheduler.flush() 并且 VirtualScheduler 执行预定的去抖动工作,这会将原始的 PROCESSING 操作传递给 mergeMap
  • 伪造ajax,同步发出响应
  • 响应映射到第二个 PROCESSING 操作
  • 您的史诗同步发出第二个动作
  • 第二个动作由你的史诗递归接收并提供给去抖动
  • debounceTime 运算符现在在 VirtualScheduler 上安排第二个动作,但是 debounceTime 运算符正在执行第一个动作中仍然的先前安排的工作。
  • 调用堆栈展开一堆,直到它到达先前计划的去抖动工作中,从第一个动作开始,而第一个动作只是 next() 的第一个动作。 debounceTimerxjs 代码然后设置 this.lastValue = null and this.hasValue = false 这是 rxjs 错误,需要在进入目的地之前完成
  • 堆栈进一步展开到 VirtualScheduler 的 运行ning flush() 方法,which now dequeues the second scheduled debounced action 因为它是同步添加的计划工作数组,在此之前刷新完成。请记住,到目前为止,我们只调用了 scheduler.flush() 一次,这是我们此时返回的函数。
  • 第二个预定的去抖动工作是 运行,但是 this.hasValue === false 因为第一个预定的去抖动工作设置了它,所以 debounceTime 运算符不会发出任何东西。
  • 堆栈展开到我们的第一个 scheduler.flush()
  • 我们console.log('CHECK CORRECT STATE FOR PROGRESS 25')
  • 所有其他 scheduler.flush() 调用什么都不做,因为没有任何安排。

这在技术上是一个错误,但是没有人 运行 进入它并不奇怪,因为 运行 没有任何延迟地同步去抖动违背了它的要点, 除了当你在测试时,当然 This ticket is basically the same thing and OJ says RxJS doesn't make reentrancy guarantees, but I that might be up for debate in this case. I've filed a PR with the fix to discuss

请记住,这个错误不会解决您关于排序的基本问题,但会阻止操作被吞没。

我不知道如果您想保持 100% 的同步行为 (VirtualScheduler),您会如何做您想做的事情。在去抖动之间,您需要某种方法来屈服于您的测试。对我来说,当我编写集成测试时,如果有的话,我很少模拟。例如让去抖实际上自然地去抖,或者通过模拟 setTimeout 来更快地推进它们,但 仍然保持它们异步 这将返回到你的测试允许你检查状态,但也让你的测试异步。


任何想要复制的人,here's the StackBlitz code I used

答案是 re-write 异步测试。另外 note-worthy 是我必须通过返回 Observable.fromPromise 而不是常规的 Observable.of 来模拟 ajax 请求,否则它们仍然会被反跳吞没。这些方面的东西(使用笑话):

describe('my integration test', () => {

  const scheduler = new VirtualTimeScheduler();

  beforeEach(() => {

    // Fake ajax responses
    const ajax = request => (
      Observable.fromPromise(new Promise((resolve) => {
        if (request.url === '/check_url_1') {
          resolve({ response: { progress: 25, check_url: '/check_url_2' } });
        } else if (request.url === '/check_url_2') {
          resolve({ response: { progress: 50, check_url: '/check_url_3' } });
        } else {
          resolve({ response: { progress: 100 } });
        }
      }))
    );

    store = configureStore(defaultState, { ajax, timerInterval: 1 });
  });

  it('should update the store properly after each call', (done) => {

    let i = 0;
    store.subscribe(() => {
      switch (i) {
        case 0:
          console.log('CHECK CORRECT STATE FOR PROGRESS 0');
          break;
        case 1:
          console.log('CHECK CORRECT STATE FOR PROGRESS 25');
          break;
        case 2:
          console.log('CHECK CORRECT STATE FOR PROGRESS 50');
          break;
        case 3:
          console.log('CHECK CORRECT STATE FOR PROGRESS 100');
          done();
          break;
        default:
      }
      i += 1;
    });

    store.dispatch({ checkUrl: '/check_url_1', progress: 0, type: 'PROCESSING' });
  });
});

我还通过将计时器间隔作为依赖项传递来将其设置为 1。在我的史诗中我是这样设置的:.debounceTime(dependencies.timerInterval || 1000)