使用 Jest 正确模拟 S3 createPresignedPost

Correctly mocking S3 createPresignedPost with Jest

我正在尝试将单元测试添加到我的代码中。努力处理处理预签名 S3 url 的函数。我的功能如下。

'use strict';

const AWS = require('aws-sdk');

AWS.config.update({
  region: process.env.AWS_DEFAULT_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});

const createPresignedPost = ({ key, contentType }) => {
  const s3 = new AWS.S3();
  const params = {
    Expires: 60,
    Bucket: process.env.AWS_BUCKET_NAME,
    Conditions: [['content-length-range', 100, 10000000]], // 100Byte - 10MB
    Fields: {
      'Content-Type': contentType,
      'Cache-Control': 'max-age=31536000',
      'Access-Control-Allow-Origin': '*',
      key
    }
  };

  return new Promise(async (resolve, reject) => {
    s3.createPresignedPost(params, (err, data) => {
      if (err) {
        reject(err);
      }
      resolve(data);
    });
  });
};

module.exports = createPresignedPost;

至少,我只想开玩笑地考虑单元测试在某种程度上涵盖了这个功能,所以我的阈值保持在 CI 允许我的代码构建所需的最低覆盖率之上。

我做了以下:

const mockedCreatePresignedPost = jest.fn(() => ({
  promise: jest.fn()
}));

jest.mock('aws-sdk', () => {
  return {
    S3: jest.fn(() => ({
      createPresignedPost: mockedCreatePresignedPost
    })),
    config: {
      update: jest.fn()
    }
  };
});
it('has to mock S3#createPresignedPost', /* async */ () => {
  process.env.AWS_BUCKET_NAME = 'test1';
  const params = {
    key: 'test2',
    contentType: 'application/json'
  };
  const s3params = {
    Bucket: 'test1',
    Conditions: [['content-length-range', 100, 10000000]],
    Expires: 60,
    Fields: {
      'Access-Control-Allow-Origin': '*',
      'Cache-Control': 'max-age=31536000',
      'Content-Type': params['contentType'],
      key: params['key']
    }
  };
  /* await */
  createPresignedPost(params);
  expect(mockedCreatePresignedPost).toHaveBeenCalledWith(s3params);
});

不过,这些测试不起作用。我得到以下不匹配:

expect(jest.fn()).toHaveBeenCalledWith(...expected)

- Expected
+ Received

  {"Bucket": "test1", "Conditions": [["content-length-range", 100, 10000000]], "Expires": 60, "Fields": {"Access-Control-Allow-Origin": "*", "Cache-Control": "max-age=31536000", "Content-Type": "application/json", "key": "test2"}},
+ [Function anonymous],

Number of calls: 1

为什么我在用 await 调用 createPresignedPost() 时遇到问题,就像它应该工作一样?我如何摆脱这个“额外的”[Function anonymous],以便我的测试通过?

您应该使用回调模拟 s3.createPresignedPost 方法的实现。然后,您应该使用 mock Errordata.

手动调用回调

此外,调用createPresignedPost有两个参数:params和匿名回调函数,可以用expect.any(Function)来表示。

例如

index.js:

'use strict';

const AWS = require('aws-sdk');

AWS.config.update({
  region: process.env.AWS_DEFAULT_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
});

const createPresignedPost = ({ key, contentType }) => {
  const s3 = new AWS.S3();
  const params = {
    Expires: 60,
    Bucket: process.env.AWS_BUCKET_NAME,
    Conditions: [['content-length-range', 100, 10000000]], // 100Byte - 10MB
    Fields: {
      'Content-Type': contentType,
      'Cache-Control': 'max-age=31536000',
      'Access-Control-Allow-Origin': '*',
      key,
    },
  };

  return new Promise((resolve, reject) => {
    s3.createPresignedPost(params, (err, data) => {
      if (err) {
        reject(err);
      }
      resolve(data);
    });
  });
};

module.exports = createPresignedPost;

index.test.js:

const createPresignedPost = require('./');

const mockedCreatePresignedPost = jest.fn();

jest.mock('aws-sdk', () => {
  return {
    S3: jest.fn(() => ({
      createPresignedPost: mockedCreatePresignedPost,
    })),
    config: {
      update: jest.fn(),
    },
  };
});

describe('68705353', () => {
  it('has to mock S3#createPresignedPost', async () => {
    mockedCreatePresignedPost.mockImplementation((params, callback) => {
      callback(null, 'mocked data');
    });
    process.env.AWS_BUCKET_NAME = 'test1';
    const params = {
      key: 'test2',
      contentType: 'application/json',
    };
    const s3params = {
      Bucket: 'test1',
      Conditions: [['content-length-range', 100, 10000000]],
      Expires: 60,
      Fields: {
        'Access-Control-Allow-Origin': '*',
        'Cache-Control': 'max-age=31536000',
        'Content-Type': params['contentType'],
        key: params['key'],
      },
    };
    const actual = await createPresignedPost(params);
    expect(actual).toEqual('mocked data');
    expect(mockedCreatePresignedPost).toHaveBeenCalledWith(s3params, expect.any(Function));
  });

  it('should handle error', async () => {
    mockedCreatePresignedPost.mockImplementation((params, callback) => {
      callback(new Error('network'));
    });
    process.env.AWS_BUCKET_NAME = 'test1';
    const params = {
      key: 'test2',
      contentType: 'application/json',
    };
    const s3params = {
      Bucket: 'test1',
      Conditions: [['content-length-range', 100, 10000000]],
      Expires: 60,
      Fields: {
        'Access-Control-Allow-Origin': '*',
        'Cache-Control': 'max-age=31536000',
        'Content-Type': params['contentType'],
        key: params['key'],
      },
    };
    await expect(createPresignedPost(params)).rejects.toThrowError('network');
    expect(mockedCreatePresignedPost).toHaveBeenCalledWith(s3params, expect.any(Function));
  });
});

测试结果:

 PASS  examples/68705353/index.test.js (8.834 s)
  68705353
    ✓ has to mock S3#createPresignedPost (4 ms)
    ✓ should handle error (4 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 index.js |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        9.648 s