什么是Trace-based测试
Trace-based测试是一种测试方法,通过触发操作并观察它的调用链路用来验证微服务系统的行为。要实施trace-based测试,需要执行以下操作:
1、在微服务系统中触发一次行为,并记录它的输出以及产生的TraceId。
2、等待微服务系统上报整个调用链到可观测平台上;
3、收集这个系统在行为发生后产生的所有调用链数据,这些数据需要包括时间信息,以及错误、异常信息。
4、验证这个行为的输出结果是否正确,调用链数据是否符合预期。
5、如果调用链信息不符合预期结果,测试失败。此时开发人员拿到了调用链数据,就能从中简单的调查分析出问题所在,并做出相应的修改。
这种trace-based测试方法能够让开发者在一种更符合真实情况的场景下同时对分布式系统中的多个组件进行测试。
对OpenTelemetry Demo进行trace-based测试
为了防止在执行结果和可观测指标上的预期外的输出,引入这个测试。测试的范围是OpenTelemetry demo的主流程,包括以下流程:
1、用户浏览商店;
2、用户选中某件商品;
3、下决定购买;
4、完成付款流程;
测试方式包括集成测试和点到点测试。包括26个trace-based的测试用例,这些测试会对执行结果和调用链路进行验证。
集成测试
集成测试是基于avajs组件进行测试。在测试用例中,对每个微服务的端点进行触发,验证响应结果,并保证调用链符合预期。
下面是对现金模块进行测试的例子:
type: Test
spec:
name: 'Currency: Convert'
description: Convert a currency
trigger:
type: grpc
grpc:
protobufFile: { { protobuf file with CurrencyService definition } }
address: { { currency service address } }
method: oteldemo.CurrencyService.Convert
request: |-
{
"from": {
"currencyCode": "USD",
"units": 330,
"nanos": 750000000
},
"toCode": "CAD"
}
specs:
- name: It converts from USD to CAD
selector:
span[name="CurrencyService/Convert" rpc.system="grpc"
rpc.method="Convert" rpc.service="CurrencyService"]
assertions:
- attr:app.currency.conversion.from = "USD"
- attr:app.currency.conversion.to = "CAD"
- name: It has more nanos than expected
selector: span[name="Test trigger"]
assertions:
- attr:response.body | json_path '$.nanos' >= 599380800
在trigger这一段定义了要触发的行为,上面的例子中发起了对oteldemo.CurrencyService.Convert方法的grpc调用,并带上了指定的负载参数。
在trigger之后,specs这一段定义了执行结果和调用链的断言。能看到2种不同的断言:
1、第一个断言是,CurrencyService会生成一个调用链span,通过检查span的2个属性(app.currency.conversion.from 和app.currency.conversion.to )以及它们的值,来判断这个服务是否接受到USD到CAD的转换操作。
2、第二个断言是关于执行结果的调用链span的,检查了执行结果中是否有nanos属性,以及它的值是否小于等于599380800。
点到点测试
点到点测试是基于前端组件Cypress。通过api来从前端调用服务,然后检查服务之间的行为以及单个调用链是否正确。
在这些测试中考虑了上面列出的主要场景“用户购买商品”,通过前端服务的api来实现以下行为:
1、进入商店时,用户查看了:商品的广告,以及商品建议;
2、用户查看了商品;
3、添加到购物车;
4、检查购物车中的商品是否正确;
5、最后使用购物车中的下单功能完成订单,这会生成一个订单、从用户的信用卡中扣款、执行发货流程并且清空购物车。
因为这个测试由一系列更小的测试用例组成,这里创建了一个transaction,其中定义了要执行的测试用例
type: Transaction
spec:
name: 'Frontend Service'
description:
Run all Frontend tests enabled in sequence, simulating a process of a user
purchasing products on the Astronomy store
steps:
- ./01-see-ads.yaml
- ./02-get-product-recommendation.yaml
- ./03-browse-product.yaml
- ./04-add-product-to-cart.yaml
- ./05-view-cart.yaml
- ./06-checking-out-cart.yaml
在执行过程中,主要关注最后一步的用户下单,因为它最复杂,它触发了几乎所有其他服务,正如jaeger截图中显示的:
在这个操作中,可以看到内部调用了多个服务,比如Frontend、checkoutService、cartService、productCatalogService、currentService以及其他。
在trace-based测试中,这是一个很好的场景。可以检查最终输出是否正确,以及这些服务之间的调用过程和调用顺序是否正确。下面定义了5组断言,检查这个功能:
1、“The frontend has been called with success” :用来检查整体输出
2、“The order was placed”,用来检查CheckoutService是否被调用,以及生成的span是否正确;
3、“The user was charged”, 用来检查PaymentService是否被调用,以及生成的span是否正确;
4、“The product was shipped”, 用来检查ShippingService是否被调用, 以及生成的span是否正确;
5、“The cart was emptied”, 用来检查CartService是否被调用,以及生成的span是否正确;
最终结果是以下的YAML,这里面触发了Checkout操作,然后验证上面的5组断言:
type: Test
spec:
name: 'Frontend: Checking out shopping cart'
description: Simulate user checking out shopping cart
trigger:
type: http
httpRequest:
url: {{frontend address}}/api/checkout
method: POST
headers:
- key: Content-Type
value: application/json
body: |
{
"userId": "2491f868-88f1-4345-8836-d5d8511a9f83",
"email": "someone@example.com",
"address": {
"streetAddress": "1600 Amphitheatre Parkway",
"state": "CA",
"country": "United States",
"city": "Mountain View",
"zipCode": "94043"
},
"userCurrency": "USD",
"creditCard": {
"creditCardCvv": 672,
"creditCardExpirationMonth": 1,
"creditCardExpirationYear": 2030,
"creditCardNumber": "4432-8015-6152-0454"
}
}
specs:
- name: 'The frontend has been called with success'
selector: span[name="Test trigger"]
assertions:
- attr:response.status = 200
- selector:
span[name="oteldemo.CheckoutService/PlaceOrder" rpc.system="grpc"
rpc.method="PlaceOrder" rpc.service="oteldemo.CheckoutService"]
name: 'The order was placed'
assertions:
- attr:app.user.id = "2491f868-88f1-4345-8836-d5d8511a9f83"
- attr:app.order.items.count = 1
- selector:
span[name="oteldemo.PaymentService/Charge" rpc.system="grpc"
rpc.method="Charge" rpc.service="oteldemo.PaymentService"]
name: 'The user was charged'
assertions:
- attr:rpc.grpc.status_code = 0
- attr:selected_spans.count >= 1
- selector:
span[name="oteldemo.ShippingService/ShipOrder" rpc.system="grpc"
rpc.method="ShipOrder" rpc.service="oteldemo.ShippingService"]
name: 'The product was shipped'
assertions:
- attr:rpc.grpc.status_code = 0
- attr:selected_spans.count >= 1
- selector:
span[name="oteldemo.CartService/EmptyCart" rpc.system="grpc"
rpc.method="EmptyCart" rpc.service="oteldemo.CartService"]
name: 'The cart was emptied'
assertions:
- attr:rpc.grpc.status_code = 0
- attr:selected_spans.count >= 1
最后可以运行这些测试用例,并得到如下的执行报告,其中展示了transaction中的每个测试文件以及上面描述的最终的Checkout步骤:
✔ Frontend Service (tracetest-server:11633/transaction/frontend-all/run/1)
✔ Frontend: See Ads (tracetest-server: 11633/test/frontend-see-adds/run/1/test)
✔ It called the frontend with success and got a valid redirectUrl for each ads
✔ It returns two ads
✔ Frontend: Get recommendations (tracetest-server: 11633/test/frontend-get-recommendation/run/1/test)
✔ It called the frontend with success
✔ It called ListRecommendations correctly and got 5 products
✔ Frontend: Browse products (tracetest-server:11633/test/frontend-browse-product/run/1/test)
✔ It called the frontend with success and got a product with valid attributes
✔ It queried the product catalog correctly for a specific product
✔ Frontend: Add product to the cart (tracetest-server:11633/test/frontend-add-product/run/1/test)
✔ It called the frontend with success
✔ It added an item correctly into the shopping cart
✔ It set the cart item correctly on the database
✔ Frontend: View cart (tracetest-server:11633/test/frontend-view-cart/run/1/test)
✔ It called the frontend with success
✔ It retrieved the cart items correctly
✔ Frontend: Checking out shopping cart (tracetest-server: 11633/test/frontend-checkout-shopping-cart/run/1/test)
✔ It called the frontend with success
✔ The order was placed
✔ The user was charged
✔ The product was shipped
✔ The cart was emptied
运行测试用例并评估OpenTelemetry Demo
可以通过Demo中的make run-tracetesting命令来运行整个测试用例。它会评估OpenTelemetry中的每个服务。
在开发这些测试用例的过程中,开发者们注意到执行结果中的一些问题。例如对Cypress测试的一些小修改,以及需要在后续进一步测试的后台api。
在EmailService中发现一个严重问题:第一次构建测试用例,并使用AVA测试工具直接调用时,生成的调用链是正确的,但是带上了http 500错误码,如jaeger所示:
但是,当它作为下单的其中一个环节来执行时,执行结果是符合预期的,如下图:
进一步挖掘监控指标和代码,开发者发现,用Ruby写的EmailService在处理邮件的时候,需要传入snake_case
的json而不是pascalCase
的json。单独测试时的输入:
{
"email": "google@example.com",
"order": {
"orderId": "505",
"shippingCost": {
"currencyCode": "USD"
}
// ...
}
}
点对点测试时,Checkout服务传入的是正确的:
{
"email": "google@example.com",
"order": {
"order_id": "505",
"shipping_cost": {
"currency_code": "USD"
}
// ...
}
}
正确调用该服务之后,能够看到正确的结果如下:
这是一个有意思的问题,因为在真实场景中也会遇到类似的问题,在trace-based测试和指标数据的帮助下,开发者能够定位并解决问题。
结论
使用trace-based测试方法,能够防止用户对OpenTelemetry Demo的改动带来系统行为的变化以及意料之外的输出。
在trace-based测试的帮助下,开源社区能够方便地为OpenTelemetry Demo增加新功能,而不用特别担心新功能带来意料之外的副作用,只需要在添加功能之后重复执行trace-based测试用例来验证系统功能。