如何在 Jest 中模拟 AbortController

Jos*_*ker 5 javascript mocking typescript jestjs redux-saga

我有一个Redux发出多个 API 请求的传奇。我用来takeLatest确保如果触发新操作,任何先前运行的传奇都会被取消。但是,这不会取消正在进行的请求,并且我们遇到了最大连接限制问题。

为了解决这个问题,我在 saga 中创建一个 AbortController 并将其传递给每个请求,以便在取消 saga 时可以中止它们(见下文):

export function* doSomething(action: Action): SagaIterator {
    const abortController = new AbortController();

    try {
        const fooResponse: FooResponse = yield call(getFoo, ..., abortController);
        ...
        const barResponse: BarResponse = yield call(getBar, ..., abortController);
    }
    catch {
        .. handle error
    }
    finally {
        if (yield cancelled()) {
            abortController.abort(); // Cancel the API call if the saga was cancelled
        }
    }
}

export function* watchForDoSomethingAction(): SagaIterator {
  yield takeLatest('action/type/app/do_something', doSomething);
}
Run Code Online (Sandbox Code Playgroud)

但是,我不确定如何检查它abortController.abort()是否被调用,因为 AbortController 是在传奇中实例化的。有办法嘲笑这个吗?

小智 6

为了测试AbortControllerabort功能,我模拟了global.AbortController我的测试内部。

例子:

const abortFn = jest.fn();

// @ts-ignore
global.AbortController = jest.fn(() => ({
  abort: abortFn,
}));

await act(async () => {
  // ... Trigger the cancel function
});

// expect the mock to be called
expect(abortFn).toBeCalledTimes(1);
Run Code Online (Sandbox Code Playgroud)


sli*_*wp2 3

您可以使用jest.spyOn(object, methodName)AbortController.prototype.abort方法创建模拟。然后,执行saga生成器,每一步进行测试。使用方法模拟取消gen.return()

\n

我的测试环境是node,所以我使用abortcontroller-polyfill来进行 polyfill AbortController

\n

例如

\n

saga.ts

\n
import { AbortController, abortableFetch } from \'abortcontroller-polyfill/dist/cjs-ponyfill\';\nimport _fetch from \'node-fetch\';\nimport { SagaIterator } from \'redux-saga\';\nimport { call, cancelled, takeLatest } from \'redux-saga/effects\';\nconst { fetch } = abortableFetch(_fetch);\n\nexport function getFoo(abortController) {\n  return fetch(\'http://localhost/api/foo\', { signal: abortController.signal });\n}\n\nexport function* doSomething(): SagaIterator {\n  const abortController = new AbortController();\n  try {\n    const fooResponse = yield call(getFoo, abortController);\n  } catch {\n    console.log(\'handle error\');\n  } finally {\n    if (yield cancelled()) {\n      abortController.abort();\n    }\n  }\n}\n\nexport function* watchForDoSomethingAction(): SagaIterator {\n  yield takeLatest(\'action/type/app/do_something\', doSomething);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

saga.test.ts

\n
import { AbortController } from \'abortcontroller-polyfill/dist/cjs-ponyfill\';\nimport { call, cancelled } from \'redux-saga/effects\';\nimport { doSomething, getFoo } from \'./saga\';\n\ndescribe(\'66588109\', () => {\n  it(\'should pass\', async () => {\n    const abortSpy = jest.spyOn(AbortController.prototype, \'abort\');\n    const gen = doSomething();\n    expect(gen.next().value).toEqual(call(getFoo, expect.any(AbortController)));\n    expect(gen.return!().value).toEqual(cancelled());\n    gen.next(true);\n    expect(abortSpy).toBeCalledTimes(1);\n    abortSpy.mockRestore();\n  });\n});\n
Run Code Online (Sandbox Code Playgroud)\n

测试结果:

\n
 PASS  src/stackoverflow/66588109/saga.test.ts\n  66588109\n    \xe2\x9c\x93 should pass (4 ms)\n\n----------|---------|----------|---------|---------|-------------------\nFile      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s \n----------|---------|----------|---------|---------|-------------------\nAll files |      75 |       50 |   33.33 |   78.57 |                   \n saga.ts  |      75 |       50 |   33.33 |   78.57 | 8,16,25           \n----------|---------|----------|---------|---------|-------------------\nTest Suites: 1 passed, 1 total\nTests:       1 passed, 1 total\nSnapshots:   0 total\nTime:        2.801 s\n
Run Code Online (Sandbox Code Playgroud)\n