what assert do in a class service in Jest - javascript

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

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",

How to mock AbortController in Jest

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

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

Jest , testing Asynchronous JS code always failing

I am trying to test my node app.js script in which I have an asynchronous request sendMessageRequest () to a function sendSmtpMessage() [ a Promise ]
app.js
const sendSmtpMessage = require("./sendSmtpMessage.js");
const keys = {....};
const mailOptions = {...}
const sendMessageRequest = async () => {
try {
const result = await sendSmtpMessage(keys,mailOptions);
console.log("... SEND MSG REQUEST FULLFILLED: ", result);
} catch(err){
console.log("... SEND MSG REQUEST FAILED: ");
}
};
sendMessageRequest();
I wrote the following app.spec.js, according to doc on Testing asynchronous code ( with async/await); but I guess my sendSmtpMessage() mocking is wrong...
app.spec.js
jest.mock("../sendSmtpMessage.js");
const sendSmtpMessage = require("../sendSmtpMessage.js");
const app = require("../app.js");
// sendSmtpMessage is a mock function
sendSmtpMessage.mockImplementation(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
(oauth2ClientMock.refreshToken !== "undefined")? resolve() : reject()
, 2000
});
})
});
describe('app', () => {
let keys, mailOptions;
beforeEach(() => {
keys = {....};
mailOptions = {....}
});
afterEach(() => {
keys = {};
mailOptions = {};
});
it("should call successfully sendMessageRequest", async () => {
// GIVEN
// WHEN
// THEN
expect.assertions(1);
await expect(sendSmtpMessage).resolves.toBe("OK");
});
it("should call unsuccessfully sendMessageRequest", async () => {
// GIVEN
// WHEN
keys.oauth.refresh_token = null;
// THEN
expect.assertions(1);
await expect(sendSmtpMessage).rejects.toBeTruthy();
});
});
As the console.log output is showing errors on both expectations in each test ( on resolve and reject )
console.log
jest --detectOpenHandles --coverage "app.spec.js"
FAIL test/app.spec.js
app
✕ should call successfully sendMessageRequest (15ms)
✕ should call unsuccessfully sendMessageRequest (2ms)
● app › should call successfully sendMessageRequest
expect(received).resolves.toBe()
received value must be a Promise.
Received:
function: [Function mockConstructor]
52 | // THEN
53 | expect.assertions(1);
> 54 | await expect(sendSmtpMessage).resolves.toBe("OK");
| ^
55 | });
56 |
57 | it("should call unsuccessfully sendMessageRequest", async () => {
at Object.toBe (node_modules/expect/build/index.js:158:13)
at Object.toBe (test/app.spec.js:54:44)
● app › should call successfully sendMessageRequest
expect.assertions(1)
Expected one assertion to be called but received zero assertion calls.
51 | // WHEN
52 | // THEN
> 53 | expect.assertions(1);
| ^
54 | await expect(sendSmtpMessage).resolves.toBe("OK");
55 | });
56 |
at Object.assertions (test/app.spec.js:53:12)
● app › should call unsuccessfully sendMessageRequest
expect(received).rejects.toBeTruthy()
received value must be a Promise.
Received:
function: [Function mockConstructor]
61 | // THEN
62 | expect.assertions(1);
> 63 | await expect(sendSmtpMessage).rejects.toBeTruthy();
| ^
64 | });
65 |
66 | });
at Object.toBeTruthy (node_modules/expect/build/index.js:203:13)
at Object.toBeTruthy (test/app.spec.js:63:43)
● app › should call unsuccessfully sendMessageRequest
expect.assertions(1)
Expected one assertion to be called but received zero assertion calls.
60 | keys.oauth.refresh_token = null;
61 | // THEN
> 62 | expect.assertions(1);
| ^
63 | await expect(sendSmtpMessage).rejects.toBeTruthy();
64 | });
65 |
at Object.assertions (test/app.spec.js:62:12)
Where am I wrong ? I don't understand very well the testing process of such plain js scripts... ( use to work with vue.js, test-utils ...)
thanks for feedback and eventually on any link to make me understanding the test unit in such case...
you are not awaiting the sendMessageRequest method call itself
const sendSmtpMessage = require("./sendSmtpMessage.js");
const keys = {....};
const mailOptions = {...}
const sendMessageRequest = async () => {
try {
const result = await sendSmtpMessage(keys,mailOptions);
console.log("... SEND MSG REQUEST FULLFILLED: ", result);
} catch(err){
console.log("... SEND MSG REQUEST FAILED: ");
}
};
(async function() {
await sendMessageRequest();
})();

stubbing method with async callback in sinon

I have a parsePDF() method that calls extractText(), which returns its results in an async callback.
the question
How do I write a test that tests only that parsePDF calls extractText once, and with whatever path argument was passed to parsePDF? (I have separate unit tests for extractText and cleanUp.)
Here's the basic structure of the parsePDF method:
Parser.parsePDF(path, callback) {
Parser.extractText(path, function gotResult(err, raw_text) {
if (err) {
callback(err)
return;
}
var clean_text = Parser.cleanUp(raw_text)
callback(null, clean_text);
});
};
what I've tried
Despite reading the Sinon documentation on callsArg, Mocha/Chai/Sinon tutorials, various SO posts such as this one about stubbing function with callback - causing test method to timeout, I still haven't grokked what's needed to write a proper test.
This attempt fails with the message
Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.
It makes sense, the callback isn't being fired
it('should call extractText() with path argument', function(done) {
sandbox.stub(Parser, 'extractText')
Parser.parsePDF('a known path', function(err, bill) {
expect(sinon).calledOnce(Parser.extractText).calledWith('a known path')
done()
});
});
But the following with yeilds() also fails with the message undefined is not a function pointing at the expect... line:
it('should call extractText() with path argument', function(done) {
sandbox.stub(UtilityBillParser, 'extractText').yields(null, 'some text')
Parser.parsePDF('a known path', function(err, bill) {
expect(sinon).calledOnce(Parser.extractText).calledWith('a known path')
done()
});
});
As does the following with .callsArg(1):
it('should call extractText() with path argument', function(done) {
sandbox.stub(UtilityBillParser, 'extractText').callsArg(1)
UtilityBillParser.parsePDF('a known path', function(err, bill) {
expect(sinon).calledOnce(UtilityBillParser.extractText).calledWith('a known path')
done()
});
});
Here is the unit test solution:
parser.js:
const Parser = {
parsePDF(path, callback) {
Parser.extractText(path, function gotResult(err, raw_text) {
if (err) {
callback(err);
return;
}
var clean_text = Parser.cleanUp(raw_text);
callback(null, clean_text);
});
},
extractText(path, callback) {
callback();
},
cleanUp(rawText) {
return "real clean text";
},
};
module.exports = Parser;
parser.test.js:
const Parser = require("./parser");
const sinon = require("sinon");
describe("Parser", () => {
afterEach(() => {
sinon.restore();
});
describe("#parsePDF", () => {
it("should clean up raw test", () => {
const callback = sinon.stub();
sinon.stub(Parser, "extractText").yields(null, "fake raw text");
sinon.stub(Parser, "cleanUp").returns("fake clean text");
Parser.parsePDF("./somepath", callback);
sinon.assert.calledWith(Parser.extractText, "./somepath", sinon.match.func);
sinon.assert.calledWith(Parser.cleanUp, "fake raw text");
sinon.assert.calledWith(callback, null, "fake clean text");
});
it("should handle err", () => {
const callback = sinon.stub();
const mError = new Error("some error");
sinon.stub(Parser, "extractText").yields(mError, null);
sinon.stub(Parser, "cleanUp").returns("fake clean text");
Parser.parsePDF("./somepath", callback);
sinon.assert.calledWith(Parser.extractText, "./somepath", sinon.match.func);
sinon.assert.calledWith(callback, mError);
});
});
});
Unit test result with coverage report:
Parser
#parsePDF
✓ should clean up raw test
✓ should handle err
2 passing (9ms)
----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------|----------|----------|----------|----------|-------------------|
All files | 93.75 | 100 | 77.78 | 93.75 | |
parser.js | 80 | 100 | 50 | 80 | 13,16 |
parser.test.js | 100 | 100 | 100 | 100 | |
----------------|----------|----------|----------|----------|-------------------|
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/30163720
Since you are stubbing out the extractText() method altogether, its callback never gets invoked, so none of the special processing associated with callbacks is required. The following works:
it('should call extractText() with path argument', function() {
sandbox.stub(Parser, 'extractText')
Parser.parsePDF('a known path', 'ignored');
sinon.assert.calledOnce(Parser.extractText)
sinon.assert.calledWith(Parser.extractText, 'a known path', sinon.match.func)
});
Using sinon.match.func in the second assertion reflects the fact that parsePDF creates its own anonymous function to pass to extractText; the best you can do is assert that some function was passed to extractText.

Categories

Resources