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

时间:2021-03-10 15:18:22

标签: javascript node.js unit-testing aws-lambda jestjs

我正在尝试对 lambda 函数进行单元测试,但不知道如何模拟 lambda callback 以使其停止代码执行。正在调用我模拟的 callback,在 lambda 的情况下,它将立即返回响应。但在我的单元测试中,它继续执行代码,但出现错误:

TypeError: Cannot read property 'body' of undefined

我对 Jest 比较陌生,所以不确定如何继续。

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(单元测试代码)

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

4 个答案:

答案 0 :(得分:1)

这里好像有两个问题


问题 1

混合asyncnon-async

lambda 函数可以是 asyncnon-async

async 处理程序使用可以返回或抛出的 async 函数。如果返回 Promise,lambda 函数将等待 Promise 解析或拒绝并返回结果。

non-async 函数使用回调作为第三个参数并返回传递给回调的结果。

在这种情况下,函数是 async 但也使用回调。它应该使用 async 函数回调函数,但不能同时使用两者。


问题 2

<块引用>

我模拟的 callback 被调用,在 lambda 的情况下会立即返回响应。

默认情况下,lambda 函数在调用回调时不会立即返回响应。

如果您使用 non-async handler,请注意“执行会一直持续到事件循环为空或函数超时。在所有事件循环任务完成之前,不会将响应发送到调用者。 "

(请注意,您可以将 callbackWaitsForEmptyEventLoop 设置为 false 以使 lambda 函数立即返回,但这不是真正的解决方案,因为进程的状态将被冻结并将重新启动下次调用时处于该确切状态,因此错误只会在下次调用时发生。)

因此最佳实践是确保 non-async lambda 函数始终能够运行完成,因为传递给回调的值在事件循环为空之前实际上不会传回。

在上面的示例中,它可能看起来在调用 callback 后执行停止,但这只是因为看起来 AWS 没有报告 {{ 1}} 调用时出错。

这是一个简单的非异步处理程序来演示:

callback

解决方案

在这种情况下,我将删除 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 }; 参数并使用纯粹的 callback 函数。

像这样:

async

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

Promise

答案 1 :(得分:0)

1-在根项目中添加文件夹 __mocks__

2-在 got.js 文件夹中添加文件 __mocks__

3-将代码添加到 got.js

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

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

enter image description here

答案 2 :(得分:0)

您需要模拟 callback 函数的实现。为了在错误处理后停止执行代码,您需要throw new Error(),并使用await expect(example.example(event, context, callback)).rejects.toThrow(error);捕获错误以避免测试失败。这样我们就可以模拟aws lambda

的行为

例如

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

测试结果:

 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

答案 3 :(得分:0)

Jest 支持使用回调的测试代码。您的测试可以接受 done 参数。

请参阅笑话文档 here

将该模式应用于您的测试可能如下所示:

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

备注

  1. 测试不再是 async 并接受 done 参数。
  2. 期望需要进入回调。
  3. 如果未调用 done(),测试将因超时而失败。
  4. 期望需要尝试/抓住它。这是我从 the documentation linked above 那里拿来的。这是回调中 expect().toEqual... 的结果。如果 expect 失败,则不会调用 done 并且测试将超时,然后您将收到超时错误而不是来自 expect 的更有用的错误。

这将使您无需完全切换即可使用 Promise。


一旦您使用了该测试和代码,您可能会在主处理程序代码中遇到控制流错误。

problem with calling the callback in the catch

在catch 中调用callback(error) 后,代码中的非错误路径挂起并失败。失败,因为捕获后结果未定义。

Jest/node 会将此报告为未解决的承诺错误并警告您:

<块引用>

将来,未处理的 promise 拒绝将以非零退出代码终止 Node.js 进程。

我的建议是,如果您要 await api 调用,那么不要使用 .catch,而是在它周围放置一个 try...catch

例如

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

或者去掉 await 并像承诺一样使用它。

例如

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