首页 > 解决方案 > 开玩笑地忽略一个被拒绝的即发即弃的承诺

问题描述

我的商店的processAction()函数以即发即弃的方式调用私有异步函数,然后执行获取。 processAction()它本身不处理任何错误处理,并且 - 在浏览器中 - 如果提取失败,外部库会处理任何和所有未捕获的承诺拒绝。

因此,如果我模拟我的 fetch 以拒绝,私有函数——我正在测试的效果——将拒绝。由于我没有对异步函数调用创建的承诺的引用,因此我无法在测试中捕获拒绝,但测试失败,因为存在未处理的拒绝。

我怎么能告诉 jest 可以接受这个调用私有函数本身而不是仅仅触发调用它的动作呢?

动作.ts

const actions = {
  doTheThing() {
    dispatch({ type: 'DO_THE_THING' });
  },
};

export default actions;

store.ts

import fetch from './fetch';

class Store {
  isFetching = false;

  // ...

  processAction({ type, payload }: { type: string, payload: any }) {
    switch (type) {
      case 'DO_THE_THING':
        this.fetchTheThing();
        break;
    }
  }

  private async fetchTheThing() {
    try {
      this.isFetching = true;
      const result = await fetch(myUrl);
      // ...
    } finally {
      this.isFetching = false;
    }
  }
}

export default new Store();

__mocks__/fetch.ts

let val: any;
interface fetch {
  __setVal(value: any): void;
}

export default async function fetch() {
  return val;
}

fetch.__setVal = function(value: any) {
  val = value;
};

store.test.ts

import actions from './actions';
import store from './store';

const fetch = (require('./fetch') as import('./__mocks__/fetch')).default;
jest.mock('./fetch');

test('it sets/unsets isFetching on failure', async () => {
  let rej: () => void;
  fetch.__setVal(new Promise((_, reject) => rej = reject));
  expect(store.isFetching).toBe(false);

  Actions.doTheThing();
  await Promise.sleep(); // helper function
  expect(store.isFetching).toBe(true);

  rej(); // <---- test fails here
  await Promise.sleep();
  expect(store.isFetching).toBe(false);
});

标签: javascriptnode.jstypescriptpromisejestjs

解决方案


processAction是同步的并且不知道 Promise,这导致了一个悬空的 Promise。悬空承诺永远不应该拒绝,因为这会导致未处理的拒绝,这是一种异常。这可能会导致应用程序崩溃,具体取决于环境。即使异常是全局处理的,这也不应该成为不处理预期错误的理由。

一个正确的方法是在拒绝fetchTheThing发生的地方明确地抑制拒绝:

  private async fetchTheThing() {
    try {
      ... 
    } catch {} finally {
      this.isFetching = false;
    }
  }

或者在这种情况下,它更像processAction是导致悬空承诺:

this.fetchTheThing().catch(() => {});

否则将调度未处理的拒绝事件。

没有它,可以通过监听事件来测试它:

  ...
  let onRej = jest.fn();
  process.once('unhandledRejection', onRej);
  rej();
  await Promise.sleep();
  expect(onRej).toBeCalled();
  expect(store.isFetching).toBe(false);

如果已经有另一个unhandledRejection侦听器,这将无法按预期工作,这在一个好的 Jest setup 中是可以预期的。如果是这种情况,唯一不会影响其他测试的解决方法是在测试之前重置它们并在之后重新添加:

let listeners;

beforeEach(() => {
  listeners = process.rawListeners('unhandledRejection');
  process.removeAllListeners('unhandledRejection');
});

afterEach(() => {
  (typeof listeners === 'function' ? [listeners] : listeners).forEach(listener => {
    process.on('unhandledRejection', listener);
  });
})

不建议这样做,使用时应自担风险,因为这表明错误处理存在更深层次的问题,这在正确设计的 JavaScript 应用程序中通常是不可接受的。


推荐阅读