admin管理员组文章数量:1389897
I'm trying to unit test a lambda function but can't figure out how to mock the lambda callback
so it stops code execution. The callback
I mock up is being called, which in the case of a lambda would immediately return the response. In my unit tests though, it continues executing code and I get the error:
TypeError: Cannot read property 'body' of undefined
I'm relatively new to Jest so not sure how to proceed.
example.js
(lambda code)
// dependencies
const got = require('got');
// lambda handler
const example = async (event, context, callback) => {
// message placeholder
let message;
// set request options
const gotOptions = {
json: {
process: event.process
},
responseType: 'json'
};
// http response data
const res = await got.post('/api/process', gotOptions).catch((error) => {
message = 'error calling process';
// log and return the error
console.log(message, error);
callback(message);
});
// res.body is causing the error in the test since
// this code still executes after callbacks triggered
message = `Process ${event.process} is: ${res.body.active}`;
callback(null, message);
};
// export example
exports.example = example;
example.test.js
(unit test code)
// get the lib we want to test
const example = require('./example');
// setup mocks
jest.mock('got');
// mock our lambda callback
const callback = jest.fn();
// import the modules we want to mock
const got = require('got');
// set default event
let event = {
process: 1
};
// set default context
const context = {};
// run before each test
beforeEach(() => {
// set default got.post response
got.post.mockReturnValue(Promise.resolve({
body: {
active: true
}
}));
});
// test artifact api
describe('[example]', () => {
...other tests that pass...
test('error calling process api', async () => {
let error = 'error calling process';
// set got mock response for this test to error
got.post.mockReturnValue(Promise.reject(error));
// function we want to test w/ mock data
await example.example(event, context, callback);
// test our callback function to see if it matches our desired expectedResponse
expect(callback).toHaveBeenCalledWith(error);
});
});
I'm trying to unit test a lambda function but can't figure out how to mock the lambda callback
so it stops code execution. The callback
I mock up is being called, which in the case of a lambda would immediately return the response. In my unit tests though, it continues executing code and I get the error:
TypeError: Cannot read property 'body' of undefined
I'm relatively new to Jest so not sure how to proceed.
example.js
(lambda code)
// dependencies
const got = require('got');
// lambda handler
const example = async (event, context, callback) => {
// message placeholder
let message;
// set request options
const gotOptions = {
json: {
process: event.process
},
responseType: 'json'
};
// http response data
const res = await got.post('https://some.url/api/process', gotOptions).catch((error) => {
message = 'error calling process';
// log and return the error
console.log(message, error);
callback(message);
});
// res.body is causing the error in the test since
// this code still executes after callbacks triggered
message = `Process ${event.process} is: ${res.body.active}`;
callback(null, message);
};
// export example
exports.example = example;
example.test.js
(unit test code)
// get the lib we want to test
const example = require('./example');
// setup mocks
jest.mock('got');
// mock our lambda callback
const callback = jest.fn();
// import the modules we want to mock
const got = require('got');
// set default event
let event = {
process: 1
};
// set default context
const context = {};
// run before each test
beforeEach(() => {
// set default got.post response
got.post.mockReturnValue(Promise.resolve({
body: {
active: true
}
}));
});
// test artifact api
describe('[example]', () => {
...other tests that pass...
test('error calling process api', async () => {
let error = 'error calling process';
// set got mock response for this test to error
got.post.mockReturnValue(Promise.reject(error));
// function we want to test w/ mock data
await example.example(event, context, callback);
// test our callback function to see if it matches our desired expectedResponse
expect(callback).toHaveBeenCalledWith(error);
});
});
Share
Improve this question
asked Mar 10, 2021 at 15:18
CoryDorningCoryDorning
1,9144 gold badges25 silver badges38 bronze badges
2
-
If you change
got.post.mockReturnValue(Promise.reject(error))
in your test togot.mockRejectedValue(error)
, does that do the trick? – DylanSp Commented Mar 15, 2021 at 15:35 -
It does not...the test fails with the following message which indicates the
catch
statement isn't being evaluated:Expected: "error calling process", Received: null, "Process 1 is: true"
– CoryDorning Commented Mar 15, 2021 at 19:43
4 Answers
Reset to default 21-add folder __mocks__
in root project
2-add file got.js
in __mocks__
folder
3-add code to got.js
:
module.exports = {
post: (url, options) => {
return new Promise((res, rej) => {
res({ body: { active: 'test' } })
})
}
}
4- in test file:
let example = require('./example');
let callback_arg1 = ''
let callback_arg2 = ''
let event = {
process: 1
};
let context = {};
let callback = (arg1, arg2) => {
callback_arg1 = arg1
callback_arg2 = arg2
};
describe('example', () => {
test('error calling process api', async () => {
await example.example(event, context, callback);
expect(callback_arg1).toBe(null)
expect(callback_arg2).toBe('Process 1 is: test')
});
});
You need to mock the implementation of the callback
function. In order to stop executing the code after error handling, you need to throw new Error()
, and use await expect(example.example(event, context, callback)).rejects.toThrow(error);
to catch the error to avoid test failure. In this way, we can simulate the behavior of aws lambda
E.g.
example.js
:
const got = require('got');
const example = async (event, context, callback) => {
let message;
const gotOptions = {
json: {
process: event.process,
},
responseType: 'json',
};
const res = await got.post('https://some.url/api/process', gotOptions).catch((error) => {
callback(error);
});
console.log('process');
message = `Process ${event.process} is: ${res.body.active}`;
callback(null, message);
};
exports.example = example;
example.test.js
:
const example = require('./example');
const got = require('got');
jest.mock('got');
const callback = jest.fn().mockImplementation((errorMsg) => {
if (errorMsg) throw new Error(errorMsg);
});
const event = { process: 1 };
const context = {};
describe('[example]', () => {
test('error calling process api', async () => {
let error = 'error calling process';
got.post.mockRejectedValueOnce(error);
await expect(example.example(event, context, callback)).rejects.toThrow(error);
expect(callback).toHaveBeenCalledWith(error);
});
test('should success', async () => {
got.post.mockResolvedValueOnce({
body: { active: true },
});
await example.example(event, context, callback);
expect(callback).toHaveBeenCalledWith(null, 'Process 1 is: true');
});
});
test result:
PASS examples/66567679/example.test.js
[example]
✓ error calling process api (5 ms)
✓ should success (10 ms)
console.log
process
at examples/66567679/example.js:17:11
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
example.js | 100 | 100 | 100 | 100 |
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.966 s, estimated 4 s
Looks like there are two issues here
Issue 1
Mixing async
and non-async
A lambda function can either be async
or non-async
.
An async
handler uses an async
function that can either return or throw. If a Promise
is returned the lambda function will wait for the Promise
to resolve or reject and return the result.
A non-async
function uses a callback as the third argument and returns the result passed to the callback.
In this case the function is async
but is also using a callback. It should use either an async
function or a callback function but not both.
Issue 2
The
callback
I mock up is being called, which in the case of a lambda would immediately return the response.
By default a lambda function does not immediately return the response when the callback is called.
If you use a non-async handler, note that "execution continues until the event loop is empty or the function times out. The response isn't sent to the invoker until all event loop tasks are finished."
(Note that you could set callbackWaitsForEmptyEventLoop
to false
to get the lambda function to return right away, but this isn't a real solution since the state of the process will be frozen and will restart at that exact state the next time it is invoked so the error would just happen on the next invocation.)
So best practice is to ensure that a non-async
lambda function is always able to run to pletion since the value passed to the callback isn't actually passed back until the event loop is empty.
In the example above it might look like execution stops after the callback
is invoked, but that is only because it looks like AWS does not report info on exceptions thrown after the callback
is called with an error.
Here is a simple non-async handler to demonstrate:
exports.handler = (event, context, callback) => {
console.log('starting'); // logged
callback('this error gets reported'); // callback called with an error
console.log('still running'); // logged
throw new Error('this error is not reported'); // not reported
console.log('ending'); // not logged
};
Solution
In this case I would just remove the callback
argument and go with a purely async
function.
Something like this:
const got = require('./got');
const example = async (event, context) => {
const gotOptions = {
json: {
process: event.process
},
responseType: 'json'
};
return got.post('https://some.url/api/process', gotOptions)
.then(res => `Process ${event.process} is: ${res.body.active}`)
.catch((error) => {
// log, format the returned error, etc.
// (or just remove the catch to return the error as-is)
console.log(error);
throw new Error(error);
});
};
exports.example = example;
Then you can test the returned Promise
directly like this:
const example = require('./example');
jest.mock('./got');
const got = require('./got');
// set default event
let event = {
process: 1
};
// set default context
const context = {};
// run before each test
beforeEach(() => {
// set default got.post response
got.post.mockReturnValue(Promise.resolve({
body: {
active: true
}
}));
});
// test artifact api
describe('[example]', () => {
test('error calling process api', async () => {
let error = 'error calling process';
// set got mock response for this test to error
got.post.mockReturnValue(Promise.reject(error));
// function we want to test w/ mock data
await expect(example.example(event, context)).rejects.toThrow(error); // SUCCESS
});
});
Jest supports testing code that uses callbacks. Your test can accept a done
parameter.
See the jest documentation here.
Applying that pattern to your test it could look like the following:
describe('[example]', () => {
test('error calling process api', done => {
const error = 'error calling process';
got.post.mockReturnValue(Promise.reject(error));
await example.example(event, context, callbackError => {
// used try...catch pattern from jest docs
try {
expect(callbackError).toEqual(error);
} catch (e) {
done(e);
}
});
});
});
Notes
- The test is no longer
async
and accepts adone
parameter. - The expectation needs to move into the callback.
- The test will fail with a timeout if
done()
is not called. - The expectation needs a try/catch around it. I took this from the documentation linked above. It's a consequence of the
expect().toEqual...
in the callback. If theexpect
fails,done
will not be called and the test will timeout, and then you'll get the timeout error rather than the more useful error from theexpect
.
This will get you going without having to switch it all over to use Promises.
Once you've played with that test and the code a bit you may run into a control flow bug in your main handler code.
After calling callback(error)
in the catch, the non-error path in the code is left hanging and fails. Failing because the result is undefined after the catch.
Jest/node will report this as an unresolved promise error and warn you that:
In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
My advice would be that if you're going to await
the api call, then instead of using .catch
, put a try...catch
around it.
E.g.
try {
const res = await got.post('https://some.url/api/process', gotOptions);
message = `Process ${event.process} is: ${res.body.active}`;
callback(null, message);
} catch (error) {
message = 'error calling process';
console.log(message, error);
callback(message);
}
Or alternatively get rid of the await
and use it like a promise.
E.g.
got.post('https://some.url/api/process', gotOptions)
.then(res => {
message = `Process ${event.process} is: ${res.body.active}`;
callback(null, message);
}).catch((error) => {
message = 'error calling process';
console.log(message, error);
callback(message);
});
本文标签: javascriptMock Lambda callback in Jest (Cannot read property body of undefined)Stack Overflow
版权声明:本文标题:javascript - Mock Lambda callback in Jest? (Cannot read property body of undefined) - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744591988a2614547.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论