最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to mock AbortController in Jest - Stack Overflow

programmeradmin3浏览0评论

I have a Redux saga that makes several API requests. I am using takeLatest to make sure that any previously running sagas are cancelled if a new action is fired. However this does not cancel in-flight requests and we are running into max connection limit issues.

To fix this I am creating an AbortController inside the saga and passing it to each request so they can be aborted if the saga is cancelled (see below):

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

However, I'm not sure how to check that abortController.abort() is called, since AbortController is instantiated inside the saga. Is there a way to mock this?

I have a Redux saga that makes several API requests. I am using takeLatest to make sure that any previously running sagas are cancelled if a new action is fired. However this does not cancel in-flight requests and we are running into max connection limit issues.

To fix this I am creating an AbortController inside the saga and passing it to each request so they can be aborted if the saga is cancelled (see below):

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

However, I'm not sure how to check that abortController.abort() is called, since AbortController is instantiated inside the saga. Is there a way to mock this?

Share Improve this question edited Jun 21, 2021 at 11:30 Lin Du 102k135 gold badges332 silver badges564 bronze badges asked Mar 11, 2021 at 18:07 Joshua BarkerJoshua Barker 1,0073 gold badges12 silver badges25 bronze badges
Add a ment  | 

2 Answers 2

Reset to default 8

In order to test the AbortController's abort function I mocked the global.AbortController inside my test.

Example:

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);

You can use jest.spyOn(object, methodName) to create mock for AbortController.prototype.abort method. Then, execute the saga generator, test it by each step. Simulate the cancellation using gen.return() method.

My test environment is node, so I use abortcontroller-polyfill to polyfill AbortController.

E.g.

saga.ts:

import { AbortController, abortableFetch } from 'abortcontroller-polyfill/dist/cjs-ponyfill';
import _fetch from 'node-fetch';
import { SagaIterator } from 'redux-saga';
import { call, cancelled, takeLatest } from 'redux-saga/effects';
const { fetch } = abortableFetch(_fetch);

export function getFoo(abortController) {
  return fetch('http://localhost/api/foo', { signal: abortController.signal });
}

export function* doSomething(): SagaIterator {
  const abortController = new AbortController();
  try {
    const fooResponse = yield call(getFoo, abortController);
  } catch {
    console.log('handle error');
  } finally {
    if (yield cancelled()) {
      abortController.abort();
    }
  }
}

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

saga.test.ts:

import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill';
import { call, cancelled } from 'redux-saga/effects';
import { doSomething, getFoo } from './saga';

describe('66588109', () => {
  it('should pass', async () => {
    const abortSpy = jest.spyOn(AbortController.prototype, 'abort');
    const gen = doSomething();
    expect(gen.next().value).toEqual(call(getFoo, expect.any(AbortController)));
    expect(gen.return!().value).toEqual(cancelled());
    gen.next(true);
    expect(abortSpy).toBeCalledTimes(1);
    abortSpy.mockRestore();
  });
});

test result:

 PASS  src/stackoverflow/66588109/saga.test.ts
  66588109
    ✓ should pass (4 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |      75 |       50 |   33.33 |   78.57 |                   
 saga.ts  |      75 |       50 |   33.33 |   78.57 | 8,16,25           
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.801 s
发布评论

评论列表(0)

  1. 暂无评论