【问题标题】:Ignore a rejected fire-and-forget promise in jest开玩笑地忽略一个被拒绝的即发即弃的承诺
【发布时间】:2021-03-16 21:27:25
【问题描述】:

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

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

我如何让 jest 接受这个不调用私有函数本身而不是仅仅触发调用它的操作?

actions.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);
});

【问题讨论】:

  • 模拟捕获异常的超级骗子全局异常处理 API - 然后像在应用程序中那样使用该模拟。这才是正确的方法。以不复制已部署环境的方式测试您的代码是浪费时间并且不会进行有效的测试。
  • 这个项目被加载到我无法控制的外壳中。 shell是处理super-duper异常处理的那个。
  • 然后模拟shell
  • try { reject(); } catch(err) { //swallow } finally { await Promise.sleep(); expect(...);
  • 永远不应该发生“被拒绝的即发即弃的承诺”之类的事情。在编写测试时发现它是好的。所以修复它。

标签: javascript node.js typescript promise jestjs


【解决方案1】:

我的函数以即发即弃的方式调用私有异步函数,并且不添加任何错误处理。

不要那样做。

外部库处理所有未捕获的 Promise 拒绝。在生产中,我希望 shell 来处理它,所以我不想在函数本身中处理它。

不要依赖这个外部库。

您应该有自己的全局错误处理函数,并在您的函数中使用。

在生产环境中,让错误处理函数简单地重新抛出异常,以便它被环境拾取,或者更好的是,如果可能的话,直接调用 shell 错误处理函数。

在测试中,您可以模拟出自己的全局处理程序,并断言它是使用预期参数调用的。

【讨论】:

    【解决方案2】:

    processAction 是同步的并且不知道 Promise,这导致了一个悬空的 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 侦听器,即can be expected in a good 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 应用程序中通常是不可接受的。

    【讨论】:

    • 老实说,我不会在这里提及 "unhandledRejection" 钩子并坚持使用 .catch(() =&gt; {}) 方法,尽管正如您指出的那样,这表明代码存在更大的设计问题,因为 DO_THE_THING 可能会失败并且没有人知道。很好的答案。
    猜你喜欢
    • 2021-01-10
    • 1970-01-01
    • 2018-12-10
    • 1970-01-01
    • 2017-03-17
    • 1970-01-01
    • 2018-11-19
    • 2021-10-11
    • 1970-01-01
    相关资源
    最近更新 更多