简体   繁体   English

在 Jest 中模拟 Lambda 回调? (无法读取未定义的属性主体)

[英]Mock Lambda callback in Jest? (Cannot read property body of undefined)

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.我正在尝试对 lambda function 进行单元测试,但无法弄清楚如何模拟 lambda callback ,因此它会停止代码执行。 The callback I mock up is being called, which in the case of a lambda would immediately return the response.正在调用我模拟的callback ,在 lambda 的情况下将立即返回响应。 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.我对 Jest 比较陌生,所以不知道如何继续。

example.js (lambda code) example.js (lambda 代码)

// 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) example.test.js (单元测试代码)

// 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);
  });
});

You need to mock the implementation of the callback function.您需要模拟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);为了在错误处理后停止执行代码,需要throw new Error() ,并使用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这样我们就可以模拟aws lambda的行为

Eg例如

example.js : 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 : 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混合asyncnon-async

A lambda function can either be async or non-async . lambda function 可以是asyncnon-async

An async handler uses an async function that can either return or throw. async处理程序使用可以返回或抛出的async function。 If a Promise is returned the lambda function will wait for the Promise to resolve or reject and return the result.如果返回Promise ,则 lambda function 将等待Promise解决或返回结果。

A non-async function uses a callback as the third argument and returns the result passed to the callback. non-async function 使用回调作为第三个参数,并返回传递给回调的结果。

In this case the function is async but is also using a callback.在这种情况下,function 是async的,但也使用回调。 It should use either an async function or a callback function but not both.它应该使用async function回调 function 但不能同时使用两者。


Issue 2第 2 期

The callback I mock up is being called, which in the case of a lambda would immediately return the response.正在调用我模拟的callback ,在 lambda 的情况下将立即返回响应。

By default a lambda function does not immediately return the response when the callback is called.默认情况下,调用回调时,lambda function不会立即返回响应。

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. "如果您使用非异步处理程序,请注意“执行将持续到事件循环为空或 function 超时。在所有事件循环任务完成之前,不会将响应发送到调用程序。

(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.) (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错误只会在下一次调用时发生。)

So best practice is to ensure that a non-async lambda function is always able to run to completion since the value passed to the callback isn't actually passed back until the event loop is empty.因此,最佳实践是确保non-async lambda function 始终能够运行到完成,因为传递给回调的值在事件循环为空之前实际上不会被传回。

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.在上面的示例中,调用callback后执行可能停止,但这仅仅是因为 AWS 似乎没有报告在callback后引发的异常信息,但出现错误。

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.在这种情况下,我只需使用纯async function 删除callback参数和 go。

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:然后可以像这样直接测试返回的Promise

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
  });
});

1-add folder __mocks__ in root project 1-在根项目中添加文件夹__mocks__

2-add file got.js in __mocks__ folder 2-在__mocks__文件夹中添加文件got.js

3-add code to got.js : 3-将代码添加到got.js

module.exports = {
    post: (url, options) => {
        return new Promise((res, rej) => {
            res({ body: { active: 'test' } })
        })
    }
}

4- in test file: 4-在测试文件中:

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')
    });
});

在此处输入图像描述

Jest supports testing code that uses callbacks. Jest 支持使用回调的测试代码。 Your test can accept a done parameter.您的测试可以接受done参数。

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.测试不再是async的,并且接受done参数。
  2. The expectation needs to move into the callback.期望需要进入回调。
  3. The test will fail with a timeout if done() is not called.如果没有调用done() ,测试将失败并超时。
  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.这是回调中expect().toEqual...的结果。 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 .如果expect失败,将不会调用done并且测试将超时,然后您将收到超时错误,而不是来自expect的更有用的错误。

This will get you going without having to switch it all over to use Promises.这将使您无需全部切换即可使用 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.一旦您使用了该测试和代码,您可能会在主处理程序代码中遇到控制流错误。

在 catch 中调用回调的问题

After calling callback(error) in the catch, the non-error path in the code is left hanging and fails.在 catch 中调用callback(error)后,代码中的非错误路径挂起并失败。 Failing because the result is undefined after the catch.失败,因为结果在捕获后未定义。

Jest/node will report this as an unresolved promise error and warn you that: Jest/node 会将此报告为未解决的 promise 错误并警告您:

In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.将来,未处理的 promise 拒绝将使用非零退出代码终止 Node.js 进程。

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.我的建议是,如果您要await api 调用,那么不要使用.catch ,而是使用try...catch

Eg例如

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.或者摆脱await并像 promise 一样使用它。

Eg例如

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);
  });

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM