typescript - 模拟 AWS 服务和 Lambda 最佳实践

标签 typescript amazon-web-services aws-lambda jestjs ts-jest

我正在开发一个简单的 AWS lambda 函数,该函数由 DynamoDB Streams 事件触发,应该转发除 REMOVE 之外的所有记录。事件到 SQS 队列。该功能按预期工作,这并不奇怪。
我想编写一个单元测试来测试当它是 DELETE 时不向 SQS 提交任何东西的行为。事件。我首先使用 aws-sdk-mock 进行了尝试。正如您在函数代码中看到的,我尝试通过在处理程序代码之外初始化 SQS 客户端来遵守 lambda 最佳实践。显然,这会阻止 aws-sdk-mock 模拟 SQS 服务(GitHub 上有一个关于此的问题:https://github.com/dwyl/aws-sdk-mock/issues/206)。
然后我尝试使用 jest 模拟 SQS,这需要更多代码才能正确处理,但我最终遇到了同样的问题,需要将 SQS 的初始化放在违反 lambda 最佳实践的处理程序函数中。
我怎样才能为这个函数写一个单元测试同时让 SQS 客户端 ( const sqs: SQS = new SQS() ) 在处理程序 之外初始化?我是在以错误的方式模拟服务还是要更改处理程序的结构以使其更易于测试?
我知道这个 lambda 函数非常简单,可能不需要单元测试,但我将不得不编写更多逻辑更复杂的 lambda,我认为这个非常适合演示这个问题。
index.ts

import {DynamoDBStreamEvent, DynamoDBStreamHandler} from "aws-lambda";
import SQS = require("aws-sdk/clients/sqs");
import DynamoDB = require("aws-sdk/clients/dynamodb");

const sqs: SQS = new SQS()

export const handleDynamoDbEvent: DynamoDBStreamHandler = async (event: DynamoDBStreamEvent, context, callback) => {
    const QUEUE_URL = process.env.TARGET_QUEUE_URL
    if (QUEUE_URL.length == 0) {
        throw new Error('TARGET_QUEUE_URL not set or empty')
    }
    await Promise.all(
        event.Records
            .filter(_ => _.eventName !== "REMOVE")
            .map((record) => {
                const unmarshalled = DynamoDB.Converter.unmarshall(record.dynamodb.NewImage);
                let request: SQS.SendMessageRequest = {
                    MessageAttributes: {
                        "EVENT_NAME": {
                            DataType: "String",
                            StringValue: record.eventName
                        }
                    },
                    MessageBody: JSON.stringify(unmarshalled),
                    QueueUrl: QUEUE_URL,
                }
                return sqs.sendMessage(request).promise()
            })
    );
}
index.spec.ts
import {DynamoDBRecord, DynamoDBStreamEvent, StreamRecord} from "aws-lambda";
import {AttributeValue} from "aws-lambda/trigger/dynamodb-stream";
import {handleDynamoDbEvent} from "./index";
import {AWSError} from "aws-sdk/lib/error";
import {PromiseResult, Request} from "aws-sdk/lib/request";
import * as SQS from "aws-sdk/clients/sqs";
import {mocked} from "ts-jest/utils";
import DynamoDB = require("aws-sdk/clients/dynamodb");


jest.mock('aws-sdk/clients/sqs', () => {
    return jest.fn().mockImplementation(() => {
        return {
            sendMessage: (params: SQS.Types.SendMessageRequest, callback?: (err: AWSError, data: SQS.Types.SendMessageResult) => void): Request<SQS.Types.SendMessageResult, AWSError> => {
                // @ts-ignore
                const Mock = jest.fn<Request<SQS.Types.SendMessageResult, AWSError>>(()=>{
                    return {
                        promise: (): Promise<PromiseResult<SQS.Types.SendMessageResult, AWSError>> => {
                            return new Promise<PromiseResult<SQS.SendMessageResult, AWSError>>(resolve => {
                                resolve(null)
                            })
                        }
                    }
                })
                return new Mock()
            }
        }
    })
});


describe.only('Handler test', () => {

    const mockedSqs = mocked(SQS, true)

    process.env.TARGET_QUEUE_URL = 'test'
    const OLD_ENV = process.env;

    beforeEach(() => {
        mockedSqs.mockClear()
        jest.resetModules();
        process.env = {...OLD_ENV};
    });

    it('should write INSERT events to SQS', async () => {
        console.log('Starting test')
        await handleDynamoDbEvent(createEvent(), null, null)
        expect(mockedSqs).toHaveBeenCalledTimes(1)
    });
})

最佳答案

我将如何处理这个问题的粗略想法:

  • 我不会在主函数中进行实际的 SQS 发送/操作,而是为消息客户端创建一个接口(interface)。像这样:
  • interface QueueClient {
        send(eventName: string, body: string): Promise<any>;
    }
    
  • 并创建一个实现该接口(interface)以与 SQS 交互的实际类:
  • class SQSQueueClient implements QueueClient {
        queueUrl: string
        sqs: SQS
    
        constructor() {
            this.queueUrl = process.env.TARGET_QUEUE_URL;
            if (this.queueUrl.length == 0) {
                throw new Error('TARGET_QUEUE_URL not set or empty')
            }
            this.sqs = new SQS();
        }
    
        send(eventName: string, body: string): Promise<any> {
            let request: SQS.SendMessageRequest = {
                MessageAttributes: {
                    "EVENT_NAME": {
                        DataType: "String",
                        StringValue: eventName
                    }
                },
                MessageBody: body,
                QueueUrl: this.queueUrl,
            }
            return this.sqs.sendMessage()
        }
    }
    
    此类了解如何将数据转换为 SQS 格式的详细信息
  • 然后我将main函数分成2个。入口点只是解析队列url,创建一个实际的sqs队列客户端实例并调用process() .主逻辑在process()
  • const queueClient = new SQSQueueClient();
    
    export const handleDynamoDbEvent: DynamoDBStreamHandler = async (event: DynamoDBStreamEvent, context, callback) => {
        return process(queueClient, event);
    }
    
    export const process = async (queueClient: QueueClient, event: DynamoDBStreamEvent) => {
        return await Promise.all(
            event.Records
                .filter(_ => _.eventName !== "REMOVE")
                .map((record) => {
                    const unmarshalled = DynamoDB.Converter.unmarshall(record.dynamodb.NewImage);
                    return queueClient.send(record.eventName, JSON.stringify(unmarshalled));
                })
        );
    }
    
  • 现在更容易测试 process() 中的主要逻辑.您可以提供一个实现接口(interface) QueueClient 的模拟实例通过手写一个或使用任何你喜欢的模拟框架
  • 对于 SQSQueueClient类,单元测试没有太多好处,所以我将更多地依赖集成测试(例如使用类似 localstack 的东西)

  • 我现在没有实际的 IDE,如果这里和那里有语法错误,请原谅我

    关于typescript - 模拟 AWS 服务和 Lambda 最佳实践,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64167062/

    相关文章:

    amazon-web-services - Mechanical Turk 中的多个 HIT 或外部问题?

    amazon-web-services - AWS CDK : how to target API Gateway API from Route53

    java - AWS API Gateway WebSocket 超时?

    java - AWS Systems Manager 参数存储 : Using StringList as Key Value Pairs in Java (Lambda)

    amazon-web-services - AWS CodeDeploy 能否为 lambda 进行跨账户部署?

    reactjs - 我的自定义身份验证 react-router 路由有什么问题?

    angular - 无法读取 nativescript angular2 中未定义的属性全局数组

    javascript - 如何将 getter 值绑定(bind)到 HTML 中?

    javascript - 获取不同语言 Angular 中数字的千位分隔符

    python - 奇怪的是,使用某些 URL 无法为 Amazon Mechanical Turk 制作 HIT?