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?
2 Answers
Reset to default 8In 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