I have the following code of the NodeJS AWS Lambda function I would like to cover by unit tests with Jest:
index.js
const { getConfig } = require('./modules/config/configManager');
const BridgeDataProvider = require('./modules/storage/bridge/bridgeDataProvider');
const ClientProcessor = require('./modules/domain/clientProcessor');
const configPromise = getConfig();
exports.handler = async () => {
const config = await configPromise;
if (!config.enabled) {
return;
}
const bridgeDataProvider = new BridgeDataProvider(config.bridgeDynamoDbConfig);
const clientProcessor = new ClientProcessor(config);
const clients = await bridgeDataProvider.getActiveClients();
for (const client of clients) {
await clientProcessor.process(client);
}
};
And I'm trying to mock getConfig
async function for each test individually, but unfortunately it doesn't work. await getConfig()
in index.js
always returns undefined
Here's the testing code:
index.test.js
const { getConfig } = require('../modules/config/configManager');
const { handler } = require('../index');
const BridgeDataProvider = require('../modules/storage/bridge/bridgeDataProvider');
const ClientProcessor = require('../modules/domain/clientProcessor');
jest.mock('../modules/config/configManager', () => ({
getConfig: jest.fn(),
}));
jest.mock('../modules/storage/bridge/bridgeDataProvider');
jest.mock('../modules/domain/clientProcessor');
const defaultMocks = {
BridgeDataProvider: {
getActiveClients: jest.fn().mockImplementation(() => {
/** @type {Client[]} */
const clients = [
{
id: '1234abc',
clientcode: 'Amazon',
},
{
id: '5678def',
clientcode: 'Facebook',
},
];
return Promise.resolve(clients);
}),
},
};
/**
* @typedef {import('../modules/config/config.types').AppConfig} AppConfig
* @typedef {import('../modules/storage/bridge/models.types').Client} Client
*/
describe('handler', () => {
beforeEach(() => {
jest.resetModules()
.clearAllMocks();
setupDefaultMocks();
});
it('does not handle clients if function is not enabled', async () => {
getConfig.mockResolvedValue({enabled: false, bridgeDynamoDbConfig: {}}); // this is not working, await getConfig() returns undefined in index.js
await handler();
const processMethod = ClientProcessor.mock.instances[0].process;
expect(processMethod).toHaveBeenCalledTimes(0);
});
});
function setupDefaultMocks() {
getConfig.mockResolvedValue({enabled: true, bridgeDynamoDbConfig: {}}); // this is not working, await getConfig() returns undefined in index.js
BridgeDataProvider.mockImplementation(() => defaultMocks.BridgeDataProvider);
}
Test output:
FAIL test/index.test.js
● handler › does not handle clients if function is not enabled
TypeError: Cannot read properties of undefined (reading 'enabled')
10 |
11 | const config = await configPromise;
> 12 | if (!config.enabled) {
| ^
13 | return;
14 | }
15 |
at enabled (index.js:12:17)
at Object.<anonymous> (test/index.test.js:48:9)
If I put default implementation right inside jest.mock('../modules/config/configManager', ...)
statement, it will mock resolved value as expected. Why is the mocking in individual test not working and how to make it work?
Thanks to @slideshowp2, he pointed me to the fact that getConfig
is called at the module scope, and I need to require handler
after doing the mock of getConfig
. However, if I try to add two tests with differently mocked getConfig
(config.enabled = true
and config.enabled = false
), second test will get the same enabled = true
. I put the example below based on the @slideshowp2's answer. I belive I need to remove the index.js
module cache after each test. I would be grateful if someone would show how to do it.
index.test.js
const { getConfig } = require('../modules/config/configManager');
const BridgeDataProvider = require('../modules/storage/bridge/bridgeDataProvider');
const ClientProcessor = require('../modules/domain/clientProcessor');
jest.mock('../modules/config/configManager');
jest.mock('../modules/storage/bridge/bridgeDataProvider');
jest.mock('../modules/domain/clientProcessor');
describe('index', () => {
test('should pass (enabled=true)', async () => {
getConfig.mockResolvedValueOnce({ enabled: true, bridgeDynamoDbConfig: 'fake bridge dynamoDB config' });
const bridgeDataProviderInstance = {
getActiveClients: jest.fn().mockResolvedValueOnce([1, 2])
};
BridgeDataProvider.mockImplementation(() => bridgeDataProviderInstance);
const clientProcessorInstance = {
process: jest.fn()
};
ClientProcessor.mockImplementation(() => clientProcessorInstance);
const {handler} = require('../index');
await handler();
expect(getConfig).toBeCalledTimes(1);
expect(BridgeDataProvider).toBeCalledWith('fake bridge dynamoDB config');
expect(bridgeDataProviderInstance.getActiveClients).toBeCalledTimes(1);
expect(clientProcessorInstance.process.mock.calls).toEqual([[1], [2]]);
});
test('should pass (enabled=false)', async () => {
getConfig.mockResolvedValueOnce({ enabled: false, bridgeDynamoDbConfig: 'fake bridge dynamoDB config' });
const bridgeDataProviderInstance = {
getActiveClients: jest.fn().mockResolvedValueOnce([1, 2])
};
BridgeDataProvider.mockImplementation(() => bridgeDataProviderInstance);
const clientProcessorInstance = {
process: jest.fn()
};
ClientProcessor.mockImplementation(() => clientProcessorInstance);
const {handler} = require('../index');
await handler();
expect(clientProcessorInstance.process).toBeCalledTimes(0);
});
});
Test output
FAIL test/index2.test.js
index
√ should pass (enabled=true) (234 ms)
× should pass (enabled=false) (3 ms)
● index › should pass (enabled=false)
expect(jest.fn()).toBeCalledTimes(expected)
Expected number of calls: 0
Received number of calls: 2
43 | const {handler} = require('../index');
44 | await handler();
> 45 | expect(clientProcessorInstance.process).toBeCalledTimes(0);
| ^
46 | });
47 | });
at Object.toBeCalledTimes (test/index2.test.js:45:49)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 2.95 s, estimated 4 s
CodePudding user response:
See es6-class-mocks#replacing-the-mock-using-mockimplementation-or-mockimplementationonce:
Note: you call getConfig
in the module scope, so make sure you mock its resolved value before the require('./')
statement.
Solution:
index.js
:
const { getConfig } = require('./modules/config/configManager');
const BridgeDataProvider = require('./modules/storage/bridge/bridgeDataProvider');
const ClientProcessor = require('./modules/domain/clientProcessor');
const configPromise = getConfig();
exports.handler = async () => {
const config = await configPromise;
if (!config.enabled) {
return;
}
const bridgeDataProvider = new BridgeDataProvider(config.bridgeDynamoDbConfig);
const clientProcessor = new ClientProcessor(config);
const clients = await bridgeDataProvider.getActiveClients();
for (const client of clients) {
await clientProcessor.process(client);
}
};
modules/config/configManager.js
:
exports.getConfig = async () => {
return { enabled: false }
}
modules/storage/bridge/bridgeDataProvider.js
:
class BridgeDataProvider {
async getActiveClients() {
return []
}
}
module.exports = BridgeDataProvider;
modules/domain/clientProcessor.js
:
class ClientProcessor {
async process(client) { }
}
module.exports = ClientProcessor;
index.test.js
:
const { getConfig } = require('./modules/config/configManager');
const BridgeDataProvider = require('./modules/storage/bridge/bridgeDataProvider');
const ClientProcessor = require('./modules/domain/clientProcessor');
jest.mock('./modules/config/configManager');
jest.mock('./modules/storage/bridge/bridgeDataProvider');
jest.mock('./modules/domain/clientProcessor');
describe('index', () => {
test('should pass', async () => {
getConfig.mockResolvedValueOnce({ enabled: true, bridgeDynamoDbConfig: 'fake bridge dynamoDB config' })
const bridgeDataProviderInstance = {
getActiveClients: jest.fn().mockResolvedValueOnce([1, 2])
}
BridgeDataProvider.mockImplementation(() => bridgeDataProviderInstance);
const clientProcessorInstance = {
process: jest.fn()
}
ClientProcessor.mockImplementation(() => clientProcessorInstance)
const { handler } = require('./');
await handler();
expect(getConfig).toBeCalledTimes(1);
expect(BridgeDataProvider).toBeCalledWith('fake bridge dynamoDB config');
expect(bridgeDataProviderInstance.getActiveClients).toBeCalledTimes(1);
expect(clientProcessorInstance.process.mock.calls).toEqual([[1], [2]])
})
})
Test result:
PASS stackoverflow/74137912/index.test.js (10.599 s)
index
✓ should pass (87 ms)
---------------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------------------|---------|----------|---------|---------|-------------------
All files | 76.19 | 50 | 28.57 | 78.95 |
74137912 | 92.86 | 50 | 100 | 92.31 |
index.js | 92.86 | 50 | 100 | 92.31 | 10
74137912/modules/config | 33.33 | 100 | 0 | 50 |
configManager.js | 33.33 | 100 | 0 | 50 | 2
74137912/modules/domain | 50 | 100 | 0 | 50 |
clientProcessor.js | 50 | 100 | 0 | 50 | 2
74137912/modules/storage/bridge | 50 | 100 | 0 | 50 |
bridgeDataProvider.js | 50 | 100 | 0 | 50 | 3
---------------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.271 s
CodePudding user response:
Thanks to @slideshowp2, he pointed me to the fact that getConfig
is called at the module scope of index.js
, and I need to require handler after mocking implementation of getConfig
. However, I got the issue of caching the results of getConfig
because getConfig
is called at the top-level of the module. That makes multiple tests dependent to each other.
Finally I found a way how to do require('../index')
in test ignoring the cached state of the module with a help of jest.isolateModules(fn)
. I added getIsolatedHandler()
which returns isolated handler
function without any caching state. Here's the final solution:
index.test.js
const { getConfig } = require('../modules/config/configManager');
const BridgeDataProvider = require('../modules/storage/bridge/bridgeDataProvider');
const ClientProcessor = require('../modules/domain/clientProcessor');
jest.mock('../modules/config/configManager');
jest.mock('../modules/storage/bridge/bridgeDataProvider');
jest.mock('../modules/domain/clientProcessor');
describe('index', () => {
test('should pass (enabled=true)', async () => {
getConfig.mockResolvedValueOnce({ enabled: true, bridgeDynamoDbConfig: 'fake bridge dynamoDB config' });
const bridgeDataProviderInstance = {
getActiveClients: jest.fn().mockResolvedValueOnce([1, 2])
};
BridgeDataProvider.mockImplementation(() => bridgeDataProviderInstance);
const clientProcessorInstance = {
process: jest.fn()
};
ClientProcessor.mockImplementation(() => clientProcessorInstance);
const handler = getIsolatedHandler();
await handler();
expect(getConfig).toBeCalledTimes(1);
expect(BridgeDataProvider).toBeCalledWith('fake bridge dynamoDB config');
expect(bridgeDataProviderInstance.getActiveClients).toBeCalledTimes(1);
expect(clientProcessorInstance.process.mock.calls).toEqual([[1], [2]]);
});
test('should pass (enabled=false)', async () => {
getConfig.mockResolvedValueOnce({ enabled: false, bridgeDynamoDbConfig: 'fake bridge dynamoDB config' });
const bridgeDataProviderInstance = {
getActiveClients: jest.fn().mockResolvedValueOnce([1, 2])
};
BridgeDataProvider.mockImplementation(() => bridgeDataProviderInstance);
const clientProcessorInstance = {
process: jest.fn()
};
ClientProcessor.mockImplementation(() => clientProcessorInstance);
const handler = getIsolatedHandler();
await handler();
expect(clientProcessorInstance.process).toBeCalledTimes(0);
});
});
function getIsolatedHandler() {
let handler;
jest.isolateModules(() => {
const index = require('../index');
handler = index.handler;
});
return handler;
}
Test output:
PASS test/index.test.js
index
√ should pass (enabled=true) (263 ms)
√ should pass (enabled=false) (254 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.289 s