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 to got.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
Add a ment  | 

4 Answers 4

Reset to default 2

1-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

  1. The test is no longer async and accepts a done parameter.
  2. The expectation needs to move into the callback.
  3. The test will fail with a timeout if done() is not called.
  4. 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 the expect 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 the expect.

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