跳转到主要内容
Chinese, Simplified

我们现在已进入无服务器功能的时代,我们不再需要担心代码的运行位置和方式。别人会为我们担心(以名义价格),我们只需要关心我们的职能,以实现他们的命运,并成为他们所能做的一切。这提出了一些与我们的测试实践相关的问题。好吧,它也提出了其他问题,但是对于这篇重要的博客文章而言。

从技术上讲,仍有讨厌的服务器。我们无法看到我们的功能正在运行的数据中心,或者在什么计算机上,或者实际上,如果它们在计算机或其他人的冰箱上运行。但是我们的函数以异步方式相互交互,将事件作为输入消耗并生成输出,这反过来导致消耗更多事件。我们在消息队列中测试与消费者和提供者的合同所做的所有想法都可以在这里同样适用。

无服务器世界中的合同测试


对于这篇文章,我们将使用AWS Lambda函数作为示例。我假设一切都适用于Google Cloud Functions和Microsoft Azure功能,但我从未使用过。

我使用的第一个Lambda函数帮助我们打破了传统单片系统的功能。我们需要添加一个功能,通过电子邮件将PDF发送给客户的客户,我们不希望必须完成大型应用程序的发布过程。解决方案是将遗留系统中最小的代码放在遗留系统中,一旦数据耗尽,我们就可以使用所有AWS服务并启用持续交付管道。

在这种情况下,我们选择更新遗留系统以编写包含S3存储桶所需的所有必要数据的JSON文件。 这是一个小小的改变,给了我们很大的灵活性。 从那里(一旦部署了更改),我们可以连接一个函数来响应存储桶事件,将PDF渲染回存储桶,然后让另一个函数响应这些事件以通过电子邮件发送它。 这真是太棒了。

直到有人没有通过电子邮件获取他们的PDF。

原始系统中模型类的一个小变化导致JSON格式发生变化,导致lambda函数失败。 没有工作的lambda函数意味着没有PDF,这反过来导致没有lambda函数调用和没有电子邮件。 没有警报意味着没有人在Cloud Watch日志中看到错误日志。 事实证明,交付经理因缺乏交付而感到非常不安。 谁知道?

看起来合同测试是有序的。 我们只需要确定合同的位置。

在这种情况下,我们可以使用与上一篇文章中描述的相同的Message Pact解决方案来测试此合同。 S3存储桶和S3事件只是传输机制。 实际合同介于我们添加到遗留系统的代码和lambda函数之间。 如果我们一直在关注,我们应该注意到将JSON文件写入S3存储桶正在跨越上下文边界,我们应该进行合同测试。 我们也应该对我们的日志发出警报。 而且喝得少,运动量更多,吃得更健康。 但这是狂野的无服务器西部的日子。 当然,这当然不是借口。

下面是使用Pact的基于异步消息的合同测试流程图:

Lambda函数是JSON消息的使用者,遗留应用程序是提供者。嗯,它比那复杂一点。该消息不是实际的JSON数据,而是包含对存储在S3存储桶上的JSON文件的引用的S3事件。 AWS S3服务和Lambda执行服务之间还有一个HTTP请求,但我们可以只关注更高级别,并假设我们通过S3存储桶传递一个异步S3事件传递给我们的函数。

Lambda函数消费者


大多数Lambda函数可能都是用Node.js编写的,但在这种情况下,我们精通Java PDF生成库,因此我们选择将其编写为Groovy JVM函数。使用基于JVM的Lambda函数的开销是可以的,因为只要PDF在下一个工作日开始时通过电子邮件发送,该函数响应事件的时间并不重要。然后我们也可以使用Spock编写测试和Pact-JVM来测试它。

消费者Pact测试基于我们的JSON数据有效负载定义消息交互,并将其包装在S3事件的模拟中。它看起来像这样:

class PoToPdfHandlerPactSpec extends Specification {

    // our Lambda function handler
    private PoToPdfHandler handler
    // mock of the service which will generate the PDF file
    private PDFGenerator pdfGenerator
    // mock of the service that will fetch the JSON from the S3 bucket
    private PurchaseOrderService orderService

    def setup() {
        pdfGenerator = Mock()
        orderService = Mock()
        handler = new PoToPdfHandler(pdfGenerator, orderService)
    }

    def 'has a contract with the Big Bad Legacy App with regards to POs'() {
        given:
        def poStream = new PactMessageBuilder().call {
            serviceConsumer 'PoToPdfHandlerLambdaFunction'
            hasPactWith 'Big Bad Legacy App'

            given('there is a valid address and email')
            expectsToReceive 'a purchase order in json format'
            withContent(contentType: 'application/json') {
                supplierName string('Test Supplier')
                supplierOrderId identifier()
                processingCentreId identifier()
                orderDate timestamp('yyyy-MM-dd\'T\'HH:mm:ss')
                lineItems minLike(1) {
                    productCode regexp(~/\d+/, '000011')
                    productDescription string('JIM BEAM WHITE LABEL COLA') // oh, yeah
                    quantityOrdered integer(20)
                }
                summary {
                    orderTotalExTax decimal(2000.0)
                    orderTotalIncTax decimal(2200.0)
                }
                supplierEmail string('TestSupplier@wild-serverless-west.com')
                senderEmail string('buyers@wild-serverless-west.com')
            }
        }

        def bucket = 'testbucket'
        def inputKey = 'po.json'
        def outputKey = 'po.pdf'

        // We need to mock out the AWS objects for this test,
        // as the handler will use the AWS SDK to fetch the
        // actual message from the S3 bucket using the information
        // from the event we receive
        Context context = [:] as Context
        def poBytes
        def mockS3Object = Mock(S3Object) {
            getObjectContent() >> { new S3ObjectInputStream(
              new ByteArrayInputStream(poBytes), null)
            }
        }

        // The event JSON we will receive
        def eventJson = [
          records: [
            [s3: [bucket: [name: bucket], object: [key: inputKey]]]
          ]
        ]
        def event = Gson.newInstance().fromJson(JsonOutput.toJson(eventJson), S3Event)

        when:
        poStream.run { Message message ->
            // The actual JSON from the message will be wrapped in an input stream
            // which will be read by our handler via the mocked AWS SDK call
            poBytes = message.contentsAsBytes()

            // An we now invoke the handler
            handler.handleRequest(event, context)
        }

        then:
        // We expect the PDF generator to be called. It means we were
        // able to correctly process the JSON from the downstream system
        1 * pdfGenerator.generate(_) >> new byte[1]
        1 * orderService.fetch(bucket, inputKey) >> mockS3Object
        1 * orderService.save(bucket, outputKey, _)
    }
}

 

该测试将预期消息设置为消息契约。然后,它通过AWS SDK调用来模拟S3事件以返回预期消息的消息有效负载。第三,它使用模拟事件调用lambda函数,然后验证调用的PDF和持久性服务。

运行此测试会导致使用我们预期的JSON格式生成Message Pact文件。这与前一篇文章中的消费者测试几乎相同(除了嘲笑AWS的东西)。但是,我们假装响应AWS事件,而不是测试从消息队列中消费消息。

旧版应用程序提供商


现在是重要的一部分。嗯,这一切都很重要。所以,现在更重要的部分。我们希望确保我们添加到遗留应用程序的代码位始终生成可由Lambda函数处理的JSON文件。我们可以通过使用我们用于消息提供程序的Message Pact验证来实现。我们创建了一个使用@PactVerifyProvider注释注释的测试方法,该注释与来自使用者测试的pact文件中的描述相匹配。此方法必须调用生成JSON数据的代码,该数据通常会写入S3存储桶并返回,以便可以根据我们的Lambda函数预期进行验证。

与大多数遗留应用程序一样,编写起来并不像我们嘲笑很多合作者来使其工作那么容易,我不会厌倦这些细节。以下是验证功能的简化版本:

class SubmittableSupplierOrderPact {

  @PactVerifyProvider('a purchase order in json format')
  String jsonForPurchaseOrder() {
    // This was the cause of our failure. A change in these classes
    // caused the generated JSON to change
    SupplierOrderView supplierOrder = supplierOrderViewFixture()

    ISupplierOrderItemView item1 = new SupplierOrderItemView()
    item1.with {
      setProductCode('1234')
      setProductDescription('Test Product 1')
      setPurchaseOrderQuantity(10)
      setListPrice(200)
      setTotalPriceExTax(100)
    }
    ISupplierOrderItemView item2 = new SupplierOrderItemView()
    item2.with {
      setProductCode('1235')
      setProductDescription('Test Product 2')
      setPurchaseOrderQuantity(50)
      setListPrice(200)
      setTotalPriceExTax(900)
    }
    List orderItems = [ item1, item2 ]

    IOrganisation organisation = [
      getEmailAddress: { 'TestSupplier@wild-serverless-west.com' }
    ] as IOrganisation

    // The model class that gets rendered to JSON
    def subject = new SubmittableSupplierOrder(supplierOrder, orderItems, organisation,
      // yikes! timezones!
      SystemParameterManager.timeZoneId)
    // This is the Object mapper used to convert the model classes to JSON. Hopefully nobody
    // changes the actual code to use something else. But as it is a legacy application, it
    // is unlikely.
    def mapper = new JSONMapperModule().provideReportObjectMapper()
    // return the JSON representation
    mapper.writeValueAsString(subject)
  }
}

 

运行Pact-JVM验证程序将导致调用函数jsonForPurchaseOrder,并根据pact文件中的消息有效内容验证结果。现在,如果从遗留应用程序生成的JSON发生变化,我们的Lambda函数无法处理它,我们将获得失败的构建。

对我们来说,这导致客户总是按照承诺获得他们的PDF,并且更快乐的交付经理。但我怀疑最后一点只是巧合。

看,马,我有一个合同测试锤!


既然我们已经对消息队列实现了合同测试,并且已经对Lambda函数调用实现了合同测试,我们就开始在我们可以使用这些异步合同测试的地方看到模式。我们还了解了Context Boundary作为进行合同测试的重要场所。

我们在一个有限的上下文中有一个服务,它创建了一个事件,需要将它传递给另一个有界上下文中的服务。您可以使用的模式是在边界上具有可以接受请求的适配器服务。但在我们的情况下,我们没有太多时间(第二个上下文中的大多数服务尚未存在),并且数据的所有权随着调用传递(数据现在由新服务拥有,只有参考存储到前一个上下文中的数据)。数据流也是此阶段的一种方式。

有人认为,通过合同测试,我们可以将JSON文档以正确的格式推送到文档存储。实质上,我们可以将文档存储视为传输机制,就像我们将S3存储桶视为一个一样。服务1A是提供者,JSON文档是消息有效负载,而另一方(服务2A)的服务是消费者。

综上所述


合同测试无服务器功能与我们之前完成的合同测试没有什么不同。他们在中间有输入,输出和东西,喜欢与常规功能(和孩子)一样行为不端。如果数据格式发生变化,任何异步调用(如响应事件而调用的Lambda函数)以及以非结构化格式(如JSON)传递的数据也可能容易失败。而且数据的格式也会发生变化。事实上,你的JSON格式可能已经改变了,你可能还没有注意到它。

与消息队列一样,当您需要更改消息格式时,知道谁正在消费您的消息以及消费它们的方式可能是一件幸事。与无服务器功能相同。知道哪些函数将响应您写入该S3存储桶的JSON文件,以便将来更容易更改该JSON文件。你将来必须改变它。或者你可能已经改变了它,你还没注意到它!

 

原文:https://dius.com.au/2018/10/01/contract-testing-serverless-and-asynchronous-applications-part-2/

本文:

讨论:请加入知识星球或者小红圈【首席架构师圈】

Article
知识星球
 
微信公众号
 
视频号