I'm setting up a Lambda function (node.js) and for example's sake, we'll keep it minimal.
module.exports = (event, context, callback) {
console.log("hello world")
}
However, I've created a function to wrap the lambda function that allows me to perform some functions that are required before each Lambda executes (I have a collection of Lambda functions that are wired up using their Serverless Application Model (SAM)). It also allows me to consolidate some of the logging and error handling across each function.
// hook.js
const connect = fn => (event, context, callback) => {
someFunction()
.then(() => fn(event, context, callback))
.then(res => callback(null, res))
.catch(error => {
// logging
callback(error)
})
}
module.exports = { connect }
// index.js
const Hook = require("./hook")
exports.handler = Hook.connect((event, context, callback) => {
console.log("hello world")
})
The logic is working well and Lambda is processing it successfully. However, I'm trying to stub this Hook.connect function using SinonJS and in need of some guidance.
I simply want to stub it to return a resolved promise, that way we can proceed to handle the code within each Lambda function (fn(event, context, callback)).
const sinon = require("sinon")
const Hook = require("./hook")
const { handler } = require("./index")
const event = {} // for simplicity sake
const context = {} // for simplicity sake
const callback = {} // for simplicity sake
describe("Hello", () => {
let connectStub
beforeEach(() => {
connectStub = sinon.stub(Hook, "connect").callsFake()
afterEach(() => {
connectStub.restore()
})
it("works", () => {
const results = handler(event, context, callback)
// assert
})
})
I've tried a few different methods, from the basic, sinon.stub(Hook, "connect"), to the more complicated where I'm trying to stub private functions inside of the hook.js file using rewire.
Any help would be appreciated -- thank you in advance.
Here is a working test:
const sinon = require('sinon');
const Hook = require('./hook');
const event = {}; // for simplicity sake
const context = {}; // for simplicity sake
const callback = {}; // for simplicity sake
describe('Hello', () => {
let handler, connectStub;
before(() => {
connectStub = sinon.stub(Hook, 'connect');
connectStub.callsFake(fn => (...args) => fn(...args)); // create the mock...
delete require.cache[require.resolve('./index')]; // (in case it's already cached)
handler = require('./index').handler; // <= ...now require index.js
});
after(() => {
connectStub.restore(); // restore Hook.connect
delete require.cache[require.resolve('./index')]; // remove the modified index.js
});
it('works', () => {
const results = handler(event, context, callback); // it works!
// assert
});
});
Details
index.js calls Hook.connect to create its exported handler as soon as it runs, and it runs as soon as it is required...
...so the mock for Hook.connect needs to be in place before index.js is required:
Node.js caches modules, so this test also clears the Node.js cache before and after the test to ensure that index.js picks up the Hook.connect mock, and to ensure that the index.js with the mocked Hook.connect is removed from the cache in case the real index.js is needed later.
Related
I have the following jest test configuration for my collection of AWS JS Node Lambdas. I have a module called dynamoStore I reference in several different lambdas package.json and use within the lambdas. I am trying to get test one of these lambdas by mocking the dynamo store module as it makes calls to dynamoDb. The problem is that the jest.fn implementation never gets called. I confirmed this by sticking a breakpoint in that line as well as logging the value the calling methods returns from it.
When I check lambda1/index.js in the debugger getVehicleMetaKeysFromDeviceId() is a jest object but when it is called it doesn't use my mock implementation
How do I get this implementation to work? Have I set up my mock incorrectly?
dynamoStore/vehicleMetaConstraints
exports.getVehicleMetaKeysFromDeviceId= async (data) => {
return data
};
dynamoStore/index.js
exports.vehicleMetaConstraints = require("./vehicleMetaConstraints");
...
lambda1/index.js
const { vehicleMetaStore } = require("dynamo-store");
exports.handler = async (event, context, callback) => {
const message = event;
let vehicle_ids = await vehicleMetaStore.getVehicleMetaKeysFromDeviceId(message.id);
// vehicle_ids end up undefined when running the test
}
lambda1/index.test.js
const { vehicleMetaStore } = require("dynamo-store");
jest.mock("dynamo-store", () => {
return {
vehicleMetaStore: {
getVehicleMetaKeysFromDeviceId: jest.fn(),
},
};
});
describe("VehicleStorageLambda", () => {
beforeEach(() => {
jest.resetModules();
process.env = { ...env };
});
afterEach(() => {
jest.clearAllMocks();
});
test("Handles first time publish with existing device", async () => {
let functionHandler = require("./index");
vehicleMetaStore.getVehicleMetaKeysFromDeviceId.mockImplementationOnce(() =>
// This never gets called
Promise.resolve({
device_id: "333936303238510e00210022",
})
);
await functionHandler.handler({});
});
});
Remove the call to jest.resetModules() in beforeEach. That's re-importing your modules before each test, and wiping out your mocks.
https://stackoverflow.com/a/59792748/3084820
I have some test code like this:
test('Test', async () => {
const someData = await setup()
const actual = myFunc(someData.x)
expect(actual.a).toEqual(someData.y)
expect(actual.b).toEqual(someData.y)
... many more like this
}
I would like to break apart the code into multiple test blocks (because I can't even add a description message to each expect statement).
If Jest supported async describe, I could do this:
describe('Some group of tests', async () => {
const someData = await setup()
test('Test1', async () => {
const actual = myFunc(someData.x)
expect(actual.a).toEqual(someData.y)
}
test('Test2', async () => {
const actual = myFunc(someData.x)
expect(actual.b).toEqual(someData.y)
}
})
I could duplicate the setup call for each test of course, but that would slow down the test considerable (I'm populating MongoDB there).
So, any way I can improve the structure of my test with Jest?
It's correct that describe callback function isn't supposed to be asynchronous. It synchronously defines tests for a suite, any asynchronous operations in its scope will be discarded.
Previously Jasmine and Jest allowed to access common test context with regular functions and this. This feature was deprecated in Jest; common variables need to be user-defined.
Shared code can be moved into helper function that internally uses beforeAll, beforeEach, etc:
const setupWithTestContext = (testContext = {}) => {
beforeAll(async () => {
const setupData = await setup();
Object.assign(testContext, setupData);
});
return testContext; // sets up a new one or returns an existing
});
const anotherSetupWithTestContext = (testContext = {}) => {
beforeEach(() => {
testContext.foo = 0;
});
return testContext;
});
...
describe('Some group of tests', async () => {
const sharedTestData = setupTestContext();
// or
// const sharedTestData = {}; setupTestContext(sharedTestData);
anotherSetupWithTestContext(sharedTestData);
test('Test1', async () => {
// context is filled with data at this point
const actual = myFunc(sharedTestData.x)
...
}
...
})
I have a pretty common testing use case and I am not sure what's the best approach there.
Context
I would like to test a module that depends on a userland dependency. The userland dependency (neat-csv) exports a single function that returns a Promise.
Goal
I want to mock neat-csv's behavior so that it rejects with an error for one single test. Then I want to restore the original module implementation.
AFAIK, I can't use jest.spyOn here as the module exports a single function.
So I thought using manual mocks was appropriated and it works. However I can't figure it out how to restore the original implementation over a manual mock.
Simplified example
For simplicity here's a stripped down version of the module I am trying to test:
'use strict';
const neatCsv = require('neat-csv');
async function convertCsvToJson(apiResponse) {
try {
const result = await neatCsv(apiResponse.body, {
separator: ';'
});
return result;
} catch (parseError) {
throw parseError;
}
}
module.exports = {
convertCsvToJson
};
And here's an attempt of testing that fails on the second test (non mocked version):
'use strict';
let neatCsv = require('neat-csv');
let { convertCsvToJson } = require('./module-under-test.js');
jest.mock('neat-csv', () =>
jest.fn().mockRejectedValueOnce(new Error('Error while parsing'))
);
const csv = 'type;part\nunicorn;horn\nrainbow;pink';
const apiResponse = {
body: csv
};
const rejectionOf = (promise) =>
promise.then(
(value) => {
throw value;
},
(reason) => reason
);
test('mocked version', async () => {
const e = await rejectionOf(convertCsvToJson(apiResponse));
expect(neatCsv).toHaveBeenCalledTimes(1);
expect(e.message).toEqual('Error while parsing');
});
test('non mocked version', async () => {
jest.resetModules();
neatCsv = require('neat-csv');
({ convertCsvToJson } = require('./module-under-test.js'));
const result = await convertCsvToJson(apiResponse);
expect(JSON.stringify(result)).toEqual(
'[{"type":"unicorn","part":"horn"},{"type":"rainbow","part":"pink"}]'
);
});
I am wondering if jest is designed to do such things or if I am going the wrong way and should inject neat-csv instead ?
What would be the idiomatic way of handling this ?
Yes, Jest is designed to do such things.
The API method you are looking for is jest.doMock. It provides a way of mocking modules without the implicit hoisting that happens with jest.mock, allowing you to mock in the scope of tests.
Here is a working example of your test code that shows this:
const csv = 'type;part\nunicorn;horn\nrainbow;pink';
const apiResponse = {
body: csv
};
const rejectionOf = promise =>
promise.then(value => {
throw value;
}, reason => reason);
test('mocked version', async () => {
jest.doMock('neat-csv', () => jest.fn().mockRejectedValueOnce(new Error('Error while parsing')));
const neatCsv = require('neat-csv');
const { convertCsvToJson } = require('./module-under-test.js');
const e = await rejectionOf(convertCsvToJson(apiResponse));
expect(neatCsv).toHaveBeenCalledTimes(1);
expect(e.message).toEqual('Error while parsing');
jest.restoreAllMocks();
});
test('non mocked version', async () => {
const { convertCsvToJson } = require('./module-under-test.js');
const result = await convertCsvToJson(apiResponse);
expect(JSON.stringify(result)).toEqual('[{"type":"unicorn","part":"horn"},{"type":"rainbow","part":"pink"}]');
});
I have an api call in a react component that looks like this.
login = () => {
// <--- If I set the localStorage on this line the test passes.
apiRequest.then(res => {
localStorage.setItem('token', res.token);
});
}
To test it I have mocked the api call. I want to check that the local storage is called, so have also mocked localStorage, however, as the localStorage is set in the mocked api call it never gets called. My test code is below. Does anyone know how I can check that the local storage is set in a mocked call. I have confirmed that if I move the localStorage outside the apiRequest it works, so it is being mocked correctly, the issue is definitely that it is in the apiRequest.
// This mocks out the api call
jest.mock('./api', () => {
return {
apiRequest: jest.fn(
() =>
new Promise(resolve => {
resolve();
})
),
};
});
const localStorageMock = (() => {
const store = {};
return {
setItem: jest.fn((key, value) => {
store[key] = value.toString();
})
}
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
});
it('sets a token in local storage', () => {
const { getByText } = render(<Login />);
const loginButton = getByText(/login/i);
// This passes
expect(apiRequest).toBeCalledTimes(1);
// This never gets called as it is being called in the apiRequest
expect(localStorage.setItem).toBeCalledWith('token', '1234');
});
If anything is unclear let me know and I will provide more details.
localStorage.setItem is called in async way through .then
login = () => {
apiRequest.then(res => {
localStorage.setItem('token', res.token);
});
}
So mocking has nothing to help with async flow. This small part
.then(res => {
localStorage.setItem('token', res.token);
}
is just put into the end of queue(it's named microtask queue if you are interested in details)
So your test code is finished and only after that this small microtask is executed.
How could you handle that? You can write test in async way and put additional expect into dedicated microtask that will run after those with localStorage.setItem call.
You can use setTimeout(macrotask) for this:
it('sets a token in local storage', done => {
const { getByText } = renderLogin();
const loginButton = getByText(/login/i);
expect(apiRequest).toBeCalledTimes(1);
setTimeout(() => {
// runs after then(....setItem) has been called
expect(localStorage.setItem).toBeCalledWith('token');
done();
}, 0);
});
or create microtask with Promise/async/await:
it('sets a token in local storage', async () => {
const { getByText } = renderLogin();
const loginButton = getByText(/login/i);
expect(apiRequest).toBeCalledTimes(1);
await Promise.resolve(); // everything below goes into separate microtask
expect(localStorage.setItem).toBeCalledWith('token');
});
[UPD] interesting thing about await that it can be used with everything else not only Promise. And it could work like Promise.resolve(<some value here>). So in your case
it('sets a token in local storage', async () => {
const { getByText } = renderLogin();
const loginButton = getByText(/login/i);
await expect(apiRequest).toBeCalledTimes(1);
expect(localStorage.setItem).toBeCalledWith('token');
});
will work as well. But I believe it looks confusing("waaaat? does .toHaveBeenCalled() return Promise for real?!") and suspicious(it's a magic! I'm not allowed to touch that!). So it's better to choose some version with straightforward "deferring"
A common problem when you try to test async code, is that you also need async tests, try to await to the apiRequest to be resolved and then verify if the local storage was called.
I'm a bit new to Sinon and having some trouble with the scenario where I need to spy on not only a function, but the functions returned by the function. Specifically, I'm trying to mock the Azure Storage SDK and ensure that once I've created a queue service, that the methods returned by the queue service are also called. Here's the example:
// test.js
// Setup a Sinon sandbox for each test
test.beforeEach(async t => {
sandbox = Sinon.sandbox.create();
});
// Restore the sandbox after each test
test.afterEach(async t => {
sandbox.restore();
});
test.only('creates a message in the queue for the payment summary email', async t => {
// Create a spy on the mock
const queueServiceSpy = sandbox.spy(AzureStorageMock, 'createQueueService');
// Replace the library with the mock
const EmailUtil = Proxyquire('../../lib/email', {
'azure-storage': AzureStorageMock,
'#noCallThru': true
});
await EmailUtil.sendBusinessPaymentSummary();
// Expect that the `createQueueService` method was called
t.true(queueServiceSpy.calledOnce); // true
// Expect that the `createMessage` method returned by
// `createQueueService` is called
t.true(queueServiceSpy.createMessage.calledOnce); // undefined
});
Here's the mock:
const Sinon = require('sinon');
module.exports = {
createQueueService: () => {
return {
createQueueIfNotExists: (queueName) => {
return Promise.resolve(Sinon.spy());
},
createMessage: (queueName, message) => {
return Promise.resolve(Sinon.spy());
}
};
}
};
I'm able to confirm that the queueServiceSpy is called once, but I'm having trouble determining if the methods returned by that method are called (createMessage).
Is there a better way to set this up or am I just missing something?
Thanks!
What you need to do is stub your service function to return a spy that you can then track calls to elsewhere. You can nest this arbitrarily deep (though I would strongly discourage that).
Something like:
const cb = sandbox.spy();
const queueServiceSpy = sandbox.stub(AzureStorageMock, 'createQueueService')
.returns({createMessage() {return cb;}}});
const EmailUtil = Proxyquire('../../lib/email', {
'azure-storage': AzureStorageMock,
'#noCallThru': true
});
await EmailUtil.sendBusinessPaymentSummary();
// Expect that the `createQueueService` method was called
t.true(queueServiceSpy.calledOnce); // true
// Expect that the `createMessage` method returned by
// `createQueueService` is called
t.true(cb.calledOnce);