How to test a unhandledRejection / uncaughtException handler with jest - javascript

I have handlers for unhandledRejections and uncaughtExceptions:
bin.js
['unhandledRejection', 'uncaughtException'].forEach(event => {
process.on(event, err => logger.error(err));
});
Now I want to test them with jest:
bin.test.js
const bin = require('../bin');
test('catches unhandled rejections', async () => {
const error = new Error('mock error');
await Promise.reject(error);
expect(logger.error).toHaveBeenCalledWith(error);
});
test('catches uncaught exceptions', () => {
const error = new Error('mock error');
throw error;
expect(logger.error).toHaveBeenCalledWith(error);
});
But jest just tells me that there are errors in the tests:
● catches unhandled rejections
mock error
8 | // https://github.com/facebook/jest/issues/5620
9 | test('catches unhandled rejections', async () => {
> 10 | const error = new Error('mock error');
| ^
11 | await Promise.reject(error);
12 | expect(logger.error).toHaveBeenCalledWith(error);
13 | });
at Object.<anonymous>.test (test/bin.test.js:10:17)
● catches uncaught exceptions
mock error
14 |
15 | test('catches uncaught exceptions', () => {
> 16 | const error = new Error('mock error');
| ^
17 | throw error;
18 | expect(logger.error).toHaveBeenCalledWith(error);
19 | });
at Object.<anonymous>.test (test/bin.test.js:16:17)
is there a way to test this?
This might be related: https://github.com/facebook/jest/issues/5620

My test strategy is to install spy onto process.on() and logger.error methods using jest.spyOn(object, methodName). After doing this, these methods have no side effects. Then, you can test your code logic in an isolated environment.
Besides, there are a few things to note:
You should spy the functions before require('./bin') statement. Because when you load the bin.js module, the code will be executed.
You should use jest.resetModules() in the beforeEach hook to resets the module registry - the cache of all required modules. Why? because require() caches its results. So, the first time a module is required, then its initialization code runs. After that, the cache just returns the value of module.exports without running the initialization code again. But we have two test cases, we want the code in module scope to be executed twice.
Now, here is the example:
bin.js:
const logger = require('./logger');
['unhandledRejection', 'uncaughtException'].forEach((event) => {
process.on(event, (err) => logger.error(err));
});
logger.js:
const logger = console;
module.exports = logger;
bin.test.js:
const logger = require('./logger');
describe('52493145', () => {
beforeEach(() => {
jest.resetModules();
});
afterEach(() => {
jest.restoreAllMocks();
});
test('catches unhandled rejections', () => {
const error = new Error('mock error');
jest.spyOn(process, 'on').mockImplementation((event, handler) => {
if (event === 'unhandledRejection') {
handler(error);
}
});
jest.spyOn(logger, 'error').mockReturnValueOnce();
require('./bin');
expect(process.on).toBeCalledWith('unhandledRejection', expect.any(Function));
expect(logger.error).toHaveBeenCalledWith(error);
});
test('catches uncaught exceptions', () => {
const error = new Error('mock error');
jest.spyOn(process, 'on').mockImplementation((event, handler) => {
if (event === 'uncaughtException') {
handler(error);
}
});
jest.spyOn(logger, 'error').mockReturnValueOnce();
require('./bin');
expect(process.on).toBeCalledWith('uncaughtException', expect.any(Function));
expect(logger.error).toHaveBeenCalledWith(error);
});
});
unit test result:
PASS examples/52493145/bin.test.js
52493145
✓ catches unhandled rejections (5 ms)
✓ catches uncaught exceptions (1 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
bin.js | 100 | 100 | 100 | 100 |
logger.js | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.73 s, estimated 4 s
source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/52493145

putting it inside try catch will help:
const error = new Error('mock error');
try {
await Promise.reject(error);
}
catch(error){
expect(logger.error).toHaveBeenCalledWith(error);
}

Related

Jest mock function is called but expect.toHaveBeenCalled() fails

I know that there are already questions about this but I can't find a definite answer. I am using SvelteKit and I tried to mock $app/navigation likes this in setup file.
jest.mock('$app/navigation', () => {
return {
__esModule: true,
goto: jest.fn().mockImplementation((target) => console.log(target))
};
});
I test a component that call goto. It is indeed called because there is a console.log call in the test output. When I tried to test it with expect(goto).toHaveBeenCalled(), it fails.
// SvelteKit
import * as navigations from '$app/navigation';
it('show error when account does not exists', async () => {
// render is in before Each
await fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'example#email.com' }
});
await fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'B#adPass0rd' }
});
await fireEvent.click(screen.getByRole('button'));
// There is no problem. It should redirect.
expect(navigations.goto).toHaveBeenCalled();
});
Output
console.log
/success
at log (jest-setup.js:6:58)
FAIL src/lib/routes-tests/login.test.js
Login
✕ show error when account does not exists (23 ms)
● Login › show error when account does not exists
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
24 | await fireEvent.click(screen.getByRole('button'));
25 | // expect(screen.queryByText('Account does not exist')).not.toBeNull();
> 26 | expect(navigations.goto).toHaveBeenCalled();
| ^
27 | });
28 | });
29 |
at toHaveBeenCalled (src/lib/routes-tests/login.test.js:26:28)
at tryCatch (src/lib/routes-tests/login.test.js:23:2404)
at Generator._invoke (src/lib/routes-tests/login.test.js:23:1964)
at Generator.next (src/lib/routes-tests/login.test.js:23:3255)
at asyncGeneratorStep (src/lib/routes-tests/login.test.js:25:103)
at _next (src/lib/routes-tests/login.test.js:27:194)
It turns out that I called goto in async function. I must use waitFor to expect the change.
await waitFor(() => expect(navigations.goto).toHaveBeenCalled())

Typescript Super is undefined when running unit tests with jest

I use super.catch inside catch from derived class. And code works fine when I do manual testing. But when I run jest unit tests it says that super is undefined. Does anyone know why? And how it can be fixed?
Code example:
export class CustomExceptionsFilter extends BaseExceptionFilter {
catch(exception: Error | HttpException, host: ArgumentsHost) {
logger.error(exception);
super.catch(exception, host);
}
}
Unit test example:
beforeEach(async () => {
filterInstance = new CustomExceptionsFilter();
host = mockArgumentsHost;
});
it('Test', async () => {
const baseClassSpy = jest.spyOn(CustomExceptionsFilter.prototype, 'catch');
const httpException = new HttpException('Test exception', 404);
filterInstance.catch(httpException, host);
expect(baseClassSpy).toHaveBeenCalled();
});
Error I get:
TypeError: Cannot read properties of undefined (reading 'reply')
35 |
36 | logger.error(exception);
> 37 | super.catch(exception, host);
| ^
38 | }
39 | }

Why java script fs.readFileSync is not getting mocked?

i have a fs.readFileSync function that i need to mock using jest.
i have tried the below code.
my original file which i want to test.
const fs = require('fs');
const read_file = (path) => {
try {
const data = fs.readFileSync(path, 'utf8');
return data;
} catch (err) {
console.error('Error in read_file', err);
throw err;
}
};
const getSecret = secretName => {
try {
return read_file(`/etc/secrets/${secretName}.txt`);
} catch (err){
throw err;
}
};
const secretConfig = {
kafka_keystore_password: getSecret('kafka_keystore_password')
};
module.exports = secretConfig;
here is my test case
const secret = require('./secret');
let fs = require('fs')
jest.mock('fs');
describe('secret read files', () => {
afterEach(jest.restoreAllMocks);
it('should read secret from file', () => {
//fs.readFileSync.mockReturnValue("randomPrivateKey");
fs.readFileSync.mockResolvedValue("randomPrivateKey")
const secretMessage = secret.kafka_keystore_password;
//expect(fs.readFileSync).toHaveBeenCalled();
expect(secretMessage).toEqual('randomPrivateKey');
})
})
describe('getSecretsFromFile', () => {
const secret = 'secret';
const secrets = { mySecret: secret };
afterEach(jest.restoreAllMocks);
it('returns secrets if file is present', () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue('randomPrivateKey');
const secretMessage = secret.kafka_keystore_password;
expect(secretMessage).toEqual('randomPrivateKey');
});
});
and i get the following error.
FAIL src/config/secret.test.js ● secret read files › should read
secret from file
expect(received).toEqual(expected) // deep equality
Expected: "randomPrivateKey"
Received: undefined
15 | const secretMessage = secret.kafka_keystore_password;
16 | //expect(fs.readFileSync).toHaveBeenCalled();
> 17 | expect(secretMessage).toEqual('randomPrivateKey');
| ^
18 |
19 | })
20 |
at Object.<anonymous> (src/config/secret.test.js:17:27)
● getSecretsFromFile › returns secrets if file is present
expect(received).toEqual(expected) // deep equality
Expected: "randomPrivateKey"
Received: undefined
32 |
33 | const secretMessage = secret.kafka_keystore_password;
> 34 | expect(secretMessage).toEqual('randomPrivateKey');
| ^
35 | });
36 | });
37 |
at Object.<anonymous> (src/config/secret.test.js:34:29)
help me fix this .
try:
const fs = {
readFileSync: jest.fn(() => ({ message: 'Test'}))
}
to mock the fs readFileSync message.
But I don't see how this test is ever going to pass because you're returning value "Test"
but checking for randomPrivateKey
expect(secretMessage).toEqual('randomPrivateKey');
Without seeing what's going on inside the code it's difficult to say but I am assuming you might want to change that line to:
expect(secretMessage).toEqual('test');
As test is the value your mocking alternatively return randomPrivateKey from the mock.
I'm having to make a load of assumptions here because I don't know-how
secret.kafka_keystore_password is calculated. I'm assuming it just returns the whatever the fs.readFileSync returns.7

Why is my Jest spy of a module's function used in another module, not getting called when testing the latter module's function?

I have a logging module set up like this:
utils/debugLogger.js
/** Logging to stderr/stdout etc using debug package */
const debug = require('debug');
// this will make sure all logging goes to console.info
// debug.log = console.info.bind(console);
const getLogger = (namespace) => {
const logger = {
log: debug(`${namespace}:log`),
debug: debug(`${namespace}:debug`),
warn: debug(`${namespace}:warning`),
info: debug(`${namespace}:info`),
error: debug(`${namespace}:error`)
}
logger.log.log = console.log.bind(console);
logger.debug.log = console.debug.bind(console);
logger.info.log = console.info.bind(console);
logger.warn.log = console.warn.bind(console);
logger.error.log = console.error.bind(console);
return logger;
}
module.exports = getLogger;
And its mock implementation:
utils/__mocks__/debugLogger.js
const getLogger = () => {
return {
error: () => {},
debug : () => {},
info : () => {},
warn: jest.fn().mockName('logger.warn-mock'),
log: () => {},
}
}
module.exports = getLogger;
And my module whose functions I want to test:
helpers/myModule.js
const logger = require('../utils/debugLogger')('helpers');
const helpers = {
getCleanTimestamp: function() {
// do something
logger.warn("something was done")
return;
},
}
module.exports = helpers;
And the test to check if the logger.info was called with "something as done":
__tests__/helpers.test.js
jest.mock(__base + 'utils/debugLogger');
const helpers = require(__base + "helpers/helpers");
const logger = require('../../utils/debugLogger')('helpers');
describe('getCleanTimestamp', () => {
it.only('should set off warnings for invalid timestamp', () => {
const logwarnSpy = jest.spyOn(logger, 'warn');
const now = helpers.getCleanTimestamp();
expect(logwarnSpy).toBeCalledWith("something was done");
});
});
I've done the following things correctly:
I've mocked the debugLogger module
did the import of debugLogger module after mocking it
But still I get this error:
FAIL server/__tests__/helpers.test.js
getCleanTimestamp
✕ should set off warnings for invalid timestamp (5 ms)
● getCleanTimestamp › should set off warnings for invalid timestamp
expect(jest.fn()).toBeCalledWith(...expected)
Expected: "something was done"
Number of calls: 0
37 | jest.spyOn(logger, 'warn');
38 | // const logwarnSpy = jest.spyOn(logger, 'warn');
> 39 | expect(logger.warn).toBeCalledWith("something was done");
| ^
40 | });
41 | });
at Object.<anonymous> (server/__tests__/helpers.test.js:39:25)
Edit: Put in the edits as prescribed in first answer. Still not working though. Getting:
● getCleanTimestamp › should set off warnings for invalid timestamp
expect(logger.warn-mock).toBeCalledWith(...expected)
Expected: "something was done"
Number of calls: 0
52 | const logInfoSpy = jest.spyOn(logger, 'warn');
53 | const now = helpers.getCleanTimestamp();
> 54 | expect(logInfoSpy).toBeCalledWith("something was done");
| ^
55 | });
56 | });
The jest.spyOn statement should happen before the action you're testing. In addition, as #jonrsharpe mentioned, the spy should be set on the info method rather than warn.
it('should set off warnings for invalid timestamp', () => {
const logInfoSpy = jest.spyOn(logger, 'info');
const now = helpers.getCleanTimestamp();
expect(logInfoSpy).toBeCalledWith("something was done");
});

jest test emitting events for eventemitter objects (http)

assume the following nodejs code
const server = http.listen(8080,'127.0.0.1')
.on("error", err => {
// ...
})
module.exports = server;
how to write a test using jest to emit the http "error" event (to cover the error event handler)?
Since you create a server in module scope, the code will execute immediately when you require or import server.js. You need to stub the http.createServer before you require this module.
For testing .on(error, callback) method, you should use mockImplementation or mockImplementationOnce, so when the mocked server calls the mocked .on('error', callback), you will get the original callback in your test case. Which means handler is equivalent to callback. When you call handler(mError), the mocked error object will be passed into the original callback. Then you can use this mError test your code logic.
Here is the unit test solution:
server.js:
const http = require('http');
const server = http.createServer();
server.listen(8080, '127.0.0.1').on('error', (err) => {
console.log(err);
});
module.exports = server;
server.test.js:
const http = require('http');
describe('60435647', () => {
it('should handle error', () => {
const mError = new Error('network');
const mServer = {
listen: jest.fn().mockReturnThis(),
on: jest.fn().mockImplementationOnce((event, handler) => {
// handler is the original callback, the mError variable will be passed into the original callback.
handler(mError);
}),
};
const createServerSpy = jest.spyOn(http, 'createServer').mockImplementationOnce(() => mServer);
const logSpy = jest.spyOn(console, 'log');
require('./server');
expect(createServerSpy).toBeCalledTimes(1);
expect(mServer.listen).toBeCalledWith(8080, '127.0.0.1');
expect(mServer.on).toBeCalledWith('error', expect.any(Function));
expect(logSpy).toBeCalledWith(mError);
});
});
Unit test results with 100% coverage:
PASS stackoverflow/60435647/server.test.js
60435647
✓ should handle error (459ms)
console.log node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
Error: network
at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60435647/server.test.js:5:20)
at Object.asyncJestTest (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:100:37)
at resolve (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
at new Promise (<anonymous>)
at mapper (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
at promise.then (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
at process._tickCallback (internal/process/next_tick.js:68:7)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
server.js | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.772s, estimated 6s

Categories

Resources