How to mock AbortController in Jest - javascript

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?

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

Related

Testing a custom UseInterval hook with jest

My hook is;
function useInterval() {
const ref: MutableRefObject<NodeJS.Timer | null > = useRef(null);
function set(callback: () => void, delay: number) {
ref.current = setInterval(callback, delay)
}
function clear() {
if (ref.current) {
clearInterval(ref.current)
ref.current = null
}
}
return { set, clear }
}
My test is;
it("set: This should be called 10 times", () => {
var callback = jest.fn();
jest.useFakeTimers()
const { result } = renderHook(() => hooks.useInterval())
act(() => {
result.current.set(() => { callback }, 100)
jest.advanceTimersByTime(1000);
})
expect(callback).toHaveBeenCalledTimes(10);
jest.useRealTimers()
})
renderHook() and act() come from "#testing-library/react-hooks": "^7.0.2"
The result I keep getting is 0 from my expect() call. I can't seem to figure out why.
If I just use setInterval() expect() gets the correct value
it("setInterval", () => {
var callback = jest.fn();
jest.useFakeTimers()
setInterval(callback, 100)
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(10);
jest.useRealTimers()
})
I have tried reordering the lines in every possible logical way I can think of.
I have noticed that I get the same result with or without act(), strangely.
Adding timers: "fake" or any of its variations(modern/legacy) to jest.config.ts doesn't seem to have any effect.
Obviously, testing-library/react-hooks is somehow masking setInterval() from jest.useFakeTimers() somehow but I don't understand how and am therefore unable to achieve the result I am looking for.
A part of me thinks that my hook isn't being hit by jest.useFakeTimers() because the fake timers are not being globally replaced, but I don't know how to do this.
Also, I'm using Typescript. Not that I think that makes a difference.
You passed an anonymous function to the set method instead of the mock callback. So the macro-task queued by setInterval will call the anonymous function. That's why the assertion fails. Nothing to Jest Config, TypeScript.
e.g.
useInterval.ts:
import { MutableRefObject, useRef } from 'react';
export function useInterval() {
const ref: MutableRefObject<ReturnType<typeof setInterval> | null> = useRef(null);
function set(callback: () => void, delay: number) {
ref.current = setInterval(callback, delay);
}
function clear() {
if (ref.current) {
clearInterval(ref.current);
ref.current = null;
}
}
return { set, clear };
}
useInterval.test.ts:
import { renderHook } from '#testing-library/react-hooks';
import { useInterval } from './useInterval';
describe('70276930', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
test('should call callback interval', () => {
const callback = jest.fn();
const { result } = renderHook(useInterval);
result.current.set(callback, 100);
jest.advanceTimersByTime(1000);
expect(callback).toBeCalledTimes(10);
});
test('should clear interval', () => {
const callback = jest.fn();
const { result } = renderHook(useInterval);
result.current.set(callback, 100);
jest.advanceTimersByTime(100);
expect(callback).toBeCalledTimes(1);
result.current.clear();
jest.advanceTimersByTime(100);
expect(callback).toBeCalledTimes(1);
});
});
test result:
PASS examples/70276930/useInterval.test.ts (7.541 s)
70276930
✓ should call callback interval (16 ms)
✓ should clear interval (1 ms)
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
useInterval.ts | 100 | 50 | 100 | 100 | 9
----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 8.294 s, estimated 9 s
package versions:
"react": "^16.14.0",
"#testing-library/react": "^11.2.2",
"jest": "^26.6.3",

Jest test fails if two methods are mocked

I have a confusing situation on my tests. See the code below.
file.js
class Test {
constructor() {}
async main() {
const a = await this.aux1("name1");
const b = await this.aux2("name2");
}
async aux1(name) {
console.log(name)
}
async aux2(name) {
console.log(name)
}
}
module.exports = Test;
file.spec.js
describe('Concept proof', () => {
const module = require("./file.js");
const inst = new module();
test('should call aux1', async () => {
const a = jest.fn(() => "return");
inst.aux1 = a
inst.main()
expect(a).toHaveBeenCalled()
});
test('should call aux2', async () => {
const b = jest.fn(() => "return");
inst.aux2 = b
inst.main()
expect(b).toHaveBeenCalled()
});
});
result
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
18 | inst.main()
19 |
> 20 | expect(b).toHaveBeenCalled()
| ^
21 | });
22 | });
at Object.<anonymous> (file.spec.js:20:19)
The method aux2 is not invoked if aux1 is called from main, but behave as expected if I remove aux1 call from main.
I was not able to find an explanation for that behave on the docs. I'm misunderstanding something, or the normal behave should be aux2 be called even if aux1 is called before?
Any help will be appreciated!
Any help will be apreciated!
You should use await inst.main();.
inst.main() is an asynchronous method. If await is not used, the main thread will continue to execute and will not wait for the completion of inst.main().
This means the expect(b).toHaveBeenCalled() statement will be executed before inst.main() method.

what assert do in a class service in Jest

I have a question related the way of test a service class that I've created:
serverRequest.service.js
import { get } from 'axios';
exports const serverRequest = {
get: (url, params) => {
try {
return get(url, params)
} catch() {
return new Error('server error');
}
}
}
serverRequest.service.spec.js
import { get } from 'axios';
import { serverRequest } from './serverRequest.service';
jest.mock('axios', () => ({
get: jest.fn(),
}));
//This part I am not sure if it is needed
const spyServerRequestGet = jest.spyOn(serverRequest, 'get');
describe('server request get', async () => {
const data = { data: {} };
get.mockReturnValue(data)
const result = await serverRequest.get('http://server', null);
// I'm not sure if this asserts have sense in here
expect(spyServerRequestGet).toHaveBeenCalledWith('http://server', null)
expect(spyServerRequestGet).toHaveBeenCalledTimes(1);
// this should be OK
expect(get).toHaveBeenCalledWith('http://server', null)
expect(result).toEqual(data);
});
My question it is about this part:
// I'm not sure if this asserts have sense in here
expect(spyServerRequestGet).toHaveBeenCalledWith('http://server', null)
expect(spyServerRequestGet).toHaveBeenCalledTimes(1);
I'm spying the method that I want test because I think that the way of the method it is used it is stuff for being tested.
What do you think about this assertion?
It is ok spying the same method that I want test?
You don't need to spy the methods which you want to test. Because you know what method(serverRequest.get) the current test case is testing.
You should spy the objects and their methods(axios.get) called by the method you want to test. Then, you can make assertions for them to check if they are called or not to ensure that your code logic and executed code branch are as expected.
E.g.
serverRequest.service.js:
import { get } from 'axios';
export const serverRequest = {
get: (url, params) => {
try {
return get(url, params);
} catch (err) {
return new Error('server error');
}
},
};
serverRequest.service.spec.js
import { get } from 'axios';
import { serverRequest } from './serverRequest.service';
jest.mock('axios', () => ({
get: jest.fn(),
}));
describe('server request get', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should pass', async () => {
const data = { data: {} };
get.mockResolvedValue(data);
const result = await serverRequest.get('http://server', null);
expect(get).toHaveBeenCalledWith('http://server', null);
expect(result).toEqual(data);
});
});
unit test result:
PASS src/stackoverflow/64069204/serverRequest.service.spec.js
server request get
✓ should pass (5ms)
--------------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------------------|----------|----------|----------|----------|-------------------|
All files | 80 | 100 | 100 | 80 | |
serverRequest.service.js | 80 | 100 | 100 | 80 | 8 |
--------------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.035s, estimated 9s

ReferenceError: Cannot access 'mockMethod1' before initialization

I have 3 source files File1.ts, File2.ts, File3.ts. While executing the Unit tests of File3 I am getting the following Error.
Test suite failed to run
ReferenceError: Cannot access 'mockMethod1' before initialization
20 | __esModule: true,
21 | default: jest.fn(),
> 22 | method1: mockMethod1,
| ^
23 | method2: mockMethod2
24 | }));
25 |
Here are the contents of the 3 source files and unit tests for File3.
File1.ts
export default class File1 {
public element;
constructor(element) {
this.element = element;
}
method1(inputs) {
// Logic of Method1.
return output;
}
method2(inputs) {
// Logic of Method2.
return output;
}
}
File2.ts
import File1 from '../Folder1/File1'
export default class File2 {
public file1Object;
constructor(element) {
this.file1Object = new File1(element);
}
method1(inputs) {
// Logic of Method1.
let out = this.file1Object.method1(inputs);
// Logic of Method1.
return output;
}
method2(inputs) {
// Logic of Method2.
let out = this.file1Object.method2(inputs);
// Logic of Method2.
return output;
}
}
File3.ts
import File2 from '../Folder2/File2'
export default class File3 {
public file2Object;
constructor(element) {
this.file2Object = new File2(element);
}
method1(inputs) {
// Logic of Method1.
let out = this.file2Object.method1(inputs);
// Logic of Method1.
return output;
}
method2(inputs) {
// Logic of Method2.
let out = this.file2Object.method1(inputs);
// Logic of Method2.
return output;
}
}
File3.test.ts
import File3 from "./File3";
import File2 from "../Folder2/File2";
const mockMethod1 = jest.fn();
const mockMethod2 = jest.fn();
jest.mock('../Folder2/File2', () => ({
__esModule: true,
default: jest.fn(),
method1: mockMethod1,
method2: mockMethod2
}));
const file3Object = new File3(inputElement);
beforeEach(() => {
jest.clearAllMocks();
});
test('Method-1 Unit Test', () => {
mockMethod1.mockReturnValue(expectedOutput);
let observedOutput = file3Object.method1(inputs);
expect(observedOutput).toBe(expectedOutput);
})
test('Method-2 Unit Test', () => {
mockMethod2.mockReturnValue(expectedOutput);
let observedOutput = file3Object.method2(inputs);
expect(observedOutput).toBe(expectedOutput);
})
I am not sure where I am making the mistake so I am unable to resolve this error. Any suggestions to resolve this issue.
There are several things that are causing trouble. First, as mentioned in jest docs:
A limitation with the factory parameter is that, since calls to jest.mock() are hoisted to the top of the file, it's not possible to first define a variable and then use it in the factory. An exception is made for variables that start with the word 'mock'. It's up to you to guarantee that they will be initialized on time!
What that means is you need to move around the lines of code to make them look like this:
// First the mock functions
const mockMethod1 = jest.fn();
const mockMethod2 = jest.fn();
// Only then your imports & jest.mock calls
import File3 from "./File3";
import File2 from "../Folder2/File2";
jest.mock('../Folder2/File2', () => ({
// ...
}));
The second issue is that you are mocking File2 as if it was exporting method1 and method2, you are not mocking method1 and method2 of the File2 class! Take a look at 4 ways of mocking an ES6 class in jest docs.

How to test async code with Jest without passing it as a callback?

Will be grateful if someone could clarify to me how test the async code from inquirer plugin for CLI app.
The module exports updateView function, which calls async inquirer.prompt inside.
const inquirer = require("inquirer");
const getAnswer = async (request) => {
const answer = await inquirer.prompt(request);
return answer;
}
Want to test with Jest that async code works, however all the Jest examples I have seen show ways to test async code only if I pass async function as a parameter.
So my function will have to be refactored to that:
getAnswers.js
const getAnswer = async (request, callback) => {
const answer = await callback(request);
return answer;
}
main.js
const inquirer = require("inquirer");
const getAnswers = require("./getAnswers");
const main = async () => {
const request = "abc";
const result = await getAnswers(request, inquirer.prompt);
...
}
And then test file will look like that:
test.js
const getAnswers = require("./getAnswers");
test("async code works", async () => {
//Arrange
const mock = async () => {
return "Correct Answer";
};
//Act
const result = await getAnswers("abc", mock);
//Assert
expect(result).toEqual("Correct Answer";);
});
Will be very grateful if someone could suggest if there is a way of testing async function without passing it as a callback?
And if the approach itself is correct.
You can use jest.mock to mock the imported dependencies rather than pass them as parameters. Here is the unit test solution:
getAnswers.js:
const inquirer = require('inquirer');
const getAnswers = async (request) => {
const answer = await inquirer.prompt(request);
return answer;
};
module.exports = getAnswers;
getAnswers.test.js:
const getAnswers = require('./getAnswers');
const inquirer = require('inquirer');
jest.mock('inquirer', () => {
return { prompt: jest.fn() };
});
describe('59495121', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should pass', async () => {
inquirer.prompt.mockResolvedValueOnce('Correct Answer');
const actual = await getAnswers('abc');
expect(actual).toBe('Correct Answer');
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/59495121/getAnswers.test.js (10.172s)
59495121
✓ should pass (6ms)
---------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
---------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
getAnswers.js | 100 | 100 | 100 | 100 | |
---------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.367s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59495121

Categories

Resources