无服务器测试策略(上)

2023-02-22   出处: medium  作/译者:Jérôme Van Der Linden/Yilia


  开始使用 AWS Lambda 和其他无服务器技术(SQS、EventBridge、Step Functions 等)的开发人员常常想知道他们应该如何测试他们的应用程序。 这不仅与测试有关,更多是与 Developer eXperience (DX) 有关:
●主要标准之一通常是速度,以及它们如何缩短代码和代码结果之间的反馈循环。 开发人员(及其经理)不想浪费时间等待编译、部署或测试执行。
●另一个基本标准是反馈的准确性:每个人都想要可靠的信息(没有不稳定的测试),并且相信如果测试正常,应用程序就会正常。
●最后,反馈的精确度也是确定错误或失败测试来源的关键。 越精确,修正的速度就越快。
  管理者通常会在这些标准之上添加一个额外的标准:成本。 我已经提到浪费时间等待,这是有代价的,但是当谈到云时,在云中使用的资源也有代价。 还可以包括编写自动化测试的成本。这篇博文描述了无服务器应用程序的不同自动化测试选项,以及它们在这 4 个方面的评价。 我会在结论中给出推荐的“策略”。
免责声明:这篇博文代表我个人的想法,并没有得到 AWS 的认可。
免责声明 2:这篇博文并非详尽无遗,并且肯定有一些用例不适合所提出的解决方案。 一种尺寸并不适合所有人!

是“测试金字塔”还是“测试蜂窝”?


  本节的目的不是提供这两者的描述和比较,而是介绍一些将在下一节中有用的词汇。 如果你对这两个概念不是很了解,请查看这些文章:Test Pyramid(作者 Martin Fowler)和 Testing of Microservices(作者 Spotify Engineering Team)。 总之,测试金字塔表明开发人员关注“单元测试”,而蜂巢则倾向于“集成测试”。 就像操作系统(Windows 与 Linux、Android 与 iOS)一样,每个操作系统都有其支持者和反对者。
  我个人喜欢单元测试,因为它们超级快、超级精确而且非常便宜,因此我更喜欢金字塔,但是:
●并不是每个人都同意单元测试的相同定义,单元和集成之间的界限在哪里。 我真的很喜欢 Martin Fowler 的这篇文章以及“社交测试”和“单独测试”的介绍:

  我们是否将社交测试视为单元测试或集成测试? 有些人会告诉单元测试应该单独验证代码,所以只有单独的测试才是真正单一的。 其他一些人会争辩说模拟是异端。 我想“这取决于”:我们是在谈论模拟另一个方法、另一个类还是整个 AWS 云?
●在无服务器应用程序中,更准确地说,在 Lambda 函数中,我们通常只有很少的业务逻辑,大部分代码都是由对其他 AWS 服务的调用组成的(在 S3 中检索文件,将消息推送到 SQS,……)。 在这种情况下,编写大量单独测试并模拟所有这些外部调用是否有意义?
我不会参与这场争论。 我只是想介绍我将在下一节中使用的这两种类型的测试。

测试选项

对于本文的其余部分,我将使用下图所示的体系结构:

  API 网关、Lambda 和 Dynamo 非常标准但又不太简单的东西:Dynamo Stream、SQS 以及与遗留数据库和后端的某种连接。 假设应用程序获取一些外部数据,对其进行处理、存储并与遗留后端同步。
请注意,所有选项都不是相互排斥的,我将在结论中给出我对这些选项的策略。

选项#1:单独测试

  正如简介的那样,单独测试允许在没有依赖关系的情况下验证一段代码,完全与外部世界隔离。 对于无服务器应用程序,这意味着你可以独立于云端测试 Lambda 函数代码。 有不同的方法可以做到这一点:

1.使用模拟库

  在测试等级中,你可以利用 Mockito(用于 Java)、sinon.js(用于 javascript)或 unittest.mock / moto(用于 python)等库来模拟依赖项。 我们通常使用它们来模拟 AWS SDK 调用。 你可以查看这篇 AWS 博客文章(“在单元测试中模拟适用于 JavaScript v3 的模块化 AWS SDK”)解释了如何使用 aws-sdk-client-mock 轻松模拟 AWS Javascript SDK v3:

  如果你要模拟的依赖项数量有限(比方说一两个 SDK 调用),这种方法非常有用。 不然,模拟所有内容会变得很痛苦:必须编写大量代码来指定依赖项应该执行和返回的内容,有时必须猜测响应的真实内容,并且可能会出错。 这让我想到了第二种方式。

2.采用六边形架构

  当 Lambda 函数增长时(注意不要鲁莽地增长)或者只是为了避免模拟 AWS 服务,可能应该考虑解耦代码:将业务逻辑与外部依赖项解耦。 六边形架构可以通过使用端口和适配器将核心业务(也称为域)与外界隔离来帮助实现这一点:

  端口为外部参与者提供与域(输入)交互以及域与外部参与者交互(输出)的能力。 简而言之,它们是接口。 适配器提供与这些外部参与者交互的逻辑,它们是实现。对于 Lambda 函数,它几乎是一样的:处理事件的函数将是一个输入适配器,而与 DynamoDB、SQS 等的交互将由输出端口和适配器处理。 如果有兴趣深入研究,请查看这篇博文(“使用 AWS Lambda 开发进化架构”)。一个明显的优势是它简化了单元测试(或更准确地说是单独测试),因为不必再模拟 AWS SDK 调用。 我们可以简单地为端口(假适配器)提供存根或模拟。 它使代码更易于生成和阅读。 它还有其他好处,比如可维护性和进化性,但它也需要一些投入。 我们一无所有!

3. 使用“本地云”框架

  你是否曾经梦想过在您的计算机中拥有云? 这是 Localstack 的承诺:“功能齐全的本地云堆栈。 离线开发和测试云和无服务器应用程序!”。 看起来很有前途,不是吗?! 你所要做的就是安装 (pip install localstack ) 并启动 (localstack start) LocalStack,并将 AWS SDK 端点更改为代码中的本地地址。 以下是 DynamoDB 的示例:

import AWS = require('aws-sdk');let docClient = new AWS.DynamoDB.DocumentClient( {
    region: "eu-west-1",
    endpoint: "http://localhost:4566"
});

  好处是不必再编写模拟:可以正在计算机上针对完整的模拟云进行测试! 也不需要在云上部署任何东西,从而减少账单。
除了现实还没有达到承诺的水平。 多种原因:
●Localstack 不提供 AWS 提供的所有功能(请参阅报道)。 有些功能不受支持,有些功能部分受支持……显然,两家公司在各自产品上工作的工程师人数并不相同。
●在部分支持的特性上,可能(实际上是有)与云有出入。 让你的测试在本地通过并不意味着它在部署到 AWS 上时也能正常工作。
●最后一点是,它的设置并不容易,你很快就会发现自己在对 LocalStack 进行故障排除而不是对您的应用程序进行故障排除,添加一些胶水代码甚至修改您自己的应用程序以针对 LocalStack 进行测试。
  我建议您查看这篇文章(“是否可以在没有云的情况下进行云开发?”)以获得有关该工具的更具体的反馈。我真的很想拥有一个“功能齐全的本地云”,但目前情况并非如此,我不推荐它。

4.总结

让我们根据介绍中给出的四个标准对单独测试进行评分(0 分最低,5 分最高):

  单独测试的主要优点是非常快速和精确(均为 5)。 另一方面,当我们完全隔离并使用模拟测试我们的代码时,我们无法保证它在部署后会按预期实际工作(准确性为 1)。 我给出的成本是 3,因为它实际上需要人工时间来设置测试、配置模拟或构建六边形架构,而且我们都(应该)知道工程师每小时的成本远高于几个 Lambda 函数 或 SQS 队列……
我的观点:
●对你的核心领域(业务逻辑)使用单独测试并避免模拟。
●喜欢干净的架构,如果不是六角形的(因为实现起来可能很繁重),至少将你的业务逻辑与所有云内容(甚至是处理程序)分开。
  查看示例,我们将在第一个和最后一个 Lambda 函数中对业务层使用一些单独的测试,以验证数据处理(转换、聚合等):

选项 #2:使用模拟 Lambda 进行社交测试

  我们现在转向社交测试,其中被测试的代码并未完全与其依赖项隔离。 在这种情况下不再有模拟。 应用于 Lambda,这意味着该函数将实际执行对部署在真实云上的真实 AWS 服务的真实调用。
  我添加了 Lambda 是“模拟”的事实:这意味着我们不调用部署在 AWS 上的真正 Lambda 函数,而是调用本地版本,以加快测试速度。 实际上,可以使用 AWS SAM(sam local invoke MyFunction)或无服务器框架(serverless invoke local -f MyFunction)来执行此操作。 在幕后,它使用 Docker 并引导类似于你选择的 AWS Lambda 运行时的环境(Java、Python 等),在其上部署您的代码并调用函数。
  我们甚至可以更进一步,在本地测试 API 网关及其与 Lambda 的集成,并使用适当的事件(使用 sam local start-api)验证函数的调用。 本地的 Step Functions 也可以完成同样的操作。 这能确保函数理解并能够解析它接收到的事件(至少来自这两个服务)。
  对于其他来源,必须查看事件示例的文档。 SAM 还可以生成其中的一些(大约 30 个),包括 DynamoDB 流事件和 SQS 消息事件,它们在我们的特定示例中都很有用:sam local generate-event dynamodb update / sam local generate-event sqs receive-message)。
  如果我们想自动化这些类型的测试,需要编写脚本,使用 SAM CLI / Serverless CLI 执行,使用 AWS CLI 进行断言。 这可能不是开发人员喜欢的(与 JUnit 或 Jest 等测试框架相比)…
  因为要再次测试真实服务,这意味着我们需要部署部分基础设施(DynamoDB/SQS/RDS),这可能有点棘手,所以通常来说我们会部署所有的东西。 在这种情况下,我们可能难以测试像 SQS 这样的服务,这些服务不是结束状态。有一个 Lambda 函数,它使用队列中的消息,因此无法断言该队列中是否存在消息。 为了克服这个问题,我们可以创建一个没有消费者的测试队列。 它对基础架构代码有一些影响。 这是 SAM 模板的示例:

AWSTemplateFormatVersion: 2010-09-09
Description: >-
  sam-sqs
Transform:
- AWS::Serverless-2016-10-31
Parameters:
  # Use the command: sam deploy --parameter-overrides 'EnvType="test"'
  # to deploy the test environment (and testing SQS queue)
  # Use sam deploy only for production deployment (in a CI/CD pipeline)
  EnvType:
    Description: Environment type.
    Default: prod
    Type: String
    AllowedValues:
      - prod
      - test
    ConstraintDescription: must specify prod or test.
Conditions:
  Testing: !Equals
    - !Ref EnvType
    - test
Resources:
  RealQueue:
    Type: AWS::SQS::Queue
  TestingQueue:
    Type: AWS::SQS::Queue
    Condition: Testing # only created when the condition is met
  SQSPublisher:
    Type: AWS::Serverless::Function
    Properties:
      Description: A Lambda function that send messages to a queue.
      Runtime: nodejs16.x
      Architectures:
        - x86_64
      Handler: src/handlers/sqs-publisher.handler
      Policies:
        - AWSLambdaBasicExecutionRole
        - SQSSendMessagePolicy:
            QueueName:
              !If [Testing, !GetAtt TestingQueue.QueueName, !GetAtt RealQueue.QueueName]
      Environment:
        Variables:
          # give the appropriate queue reference according to the condition (test or not)
          SQS_QUEUE: !If [Testing, !Ref TestingQueue, !Ref RealQueue]
  SQSConsumer:
    Type: AWS::Serverless::Function
    Properties:
      Description: A Lambda function that logs the payload of messages sent to an associated SQS queue.
      Runtime: nodejs16.x
      Architectures:
        - x86_64
      Handler: src/handlers/sqs-payload-logger.handleroked
      Events:
        SQSQueueEvent:
          Type: SQS
          Properties:
            # This function remains plugged to the real queue
            Queue: !GetAtt RealQueue.Arn
      MemorySize: 128
      Timeout: 25
      Policies:
        - AWSLambdaBasicExecutionRole

查看示例,得到下图:

  在定价方面,每月可以在 SQS 上免费获得 100 万条消息、100 万次 Lambda 执行和 250 万次 DynamoDB 流读取。 如果 DynamoDB 和 SQS 在测试环境中相当便宜,必须谨慎选择 RDS 或 Opensearch 实例类型。 AWS 通常提供免费套餐(例如,RDS db.t2.micro 750 小时 + 20 GB 存储或 OpenSearch t3.small.search 750 小时 + 10 GB 存储)但绝对需要注意。
如果我们尝试根据以下四个标准对此类测试进行评级:

●这些测试较慢 (3):必须部署基础设施(至少一次)并且本地调用(启动 Docker,“部署”功能)比仅运行单元测试要慢得多。
●对于Lambda 函数的交互,准确性要好一些 (3),但仍然处于模拟环境中,没有验证权限。
●精度也相当不错 (4),即使不是在单元测试级别,因为不只测试特定代码段,而是测试整个功能。
●至于成本,我保留 3,因为由于免费套餐,可以测试大部分架构。 但自动化测试也更复杂,需要更多时间。
我的观点:
●不要将测试策略基于这些测试,不要试图基于这些测试进行强大的覆盖。 它们不容易自动化并且有点慢(使用 CLI 工具)。
●你最终可以使用 SAM 或无服务器 CLI 在本地执行和调试(断点/逐步)函数。

选项#2bis:

  社交测试我们无法大规模利用选项 #2 中提到的 CLI 工具并使用它们构建完整的测试工具(太慢且难以自动化)。 因此,与其使用它们,不如简单地调用我们的 Lambda 函数的处理程序方法,而是保持对真实服务的调用。 除了执行的操作外,图片与上一张非常相似:

这样做有两个好处:
●首先,就开发人员体验而言,这非常重要:我们可以利用标准测试库。 用喜欢的语言和常用的工具编写测试要容易得多。 显然可以使用 SAM CLI 生成事件并将它们导入测试资源中。
  自我宣传:在 Java 中,你可以使用我编写的库 (aws-lambda-java-tests) 通过在测试中注入事件来简化测试编写:

@ParameterizedTest
@Event(value = "sqs/sqs_event.json", type = SQSEvent.class)
public void testInjectSQSEvent(SQSEvent event) {
    // test your handleRequest method with this event as parameter
}

或者更强大的:

@ParameterizedTest
@HandlerParams(
   events = @Events(folder = "apigw/events/", type = APIGatewayProxyRequestEvent.class),
   responses = @Responses(folder = "apigw/responses/", type = APIGatewayProxyResponseEvent.class))
public void testMultipleEventsResponsesInFolder(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) {
  // will inject multiple events (API GW requests) and assert responses of the Lambda 
}

●另一个优势是测试速度:没有模拟的 Lambda 环境来引导,没有 Docker。 你仍然需要部署其余的基础设施,因为我们要针对云进行测试,但是一旦部署并且如果只是修改函数的代码,它仍然非常快。
对这个选项的评价与前一个选项非常相似,除了我刚刚解释的两个优点:

●速度提高,所以是 4。
●降低实施的复杂性,从而降低成本,也移动到 4。
●准确度和精密度保持不变。
我的观点 :
●喜欢这种测试,因为它们在每个标准上都提供了很好的平衡。
●不要仅仅依赖这些测试,其中仍然缺少一些部分(权限、真实事件/触发器),请参阅选项 #3 或 #4。


声明:本文为本站编辑转载,文章版权归原作者所有。文章内容为作者个人观点,本站只提供转载参考(依行业惯例严格标明出处和作译者),目的在于传递更多专业信息,普惠测试相关从业者,开源分享,推动行业交流和进步。 如涉及作品内容、版权和其它问题,请原作者及时与本站联系(QQ:1017718740),我们将第一时间进行处理。本站拥有对此声明的最终解释权!欢迎大家通过新浪微博(@测试窝)或微信公众号(测试窝)关注我们,与我们的编辑和其他窝友交流。
187° /1872 人阅读/0 条评论 发表评论

登录 后发表评论