3.1 接口测试简介
和单体应用相比,微服务的接口测试在环境搭建上显得更加复杂。微服务系统中的单个服务大多拥有关联服务,因此在对单个服务进行测试时,对其依赖服务也要做相关准备。当依赖服务没有可供直接使用的测试环境时,还需自建虚拟服务或者上测试挡板。(服务虚拟化的内容会在本书第7章详细探讨。)
除此之外,测试数据的准备也略显复杂,这主要因为单个服务对上游数据存在一定依赖。测试人员需要提前准备好充足的被测服务所依赖的各种数据。实际操作过程中,可能会在数据库插入依赖数据,也可能需要构造特定格式的MQS消息队列数据,只有当服务消费特定消息后才能生成所需的测试数据。打个比方,拿一个交易的接口来说,由于交易数据和客户数据相关联,它的测试准备工作流程是这样的:首先,在客户表创建客户信息,可以通过执行SQL来完成;接着,构建消息体发送到MQS中生成所需的测试数据,这主要由于,与交易相关联的供应商信息是从上游服务通过MQS的方式同步来的,同时,交易关联的物流信息也需要触发MQS消息后生成;最后,在本微服务数据库构建一些其他关联数据。在这一切准备妥当之后,才能开始进行测试。
3.1.1 接口说明文档与测试用例类型
测试前,测试工程师要先了解这个接口的使用方式,通常开发团队会提供接口的说明文档,如图3-1中使用Swagger生成的接口文档,其中有接口的调用地址、入参、返回结果等信息。
图3-1 Swagger生成的接口文档
接口文档内容通常包括:
- 接口名称,一般提供有业务意义的名称方便使用者理解;
- 接口的调用地址和支持的调用方法;
- 请求参数类型,允许的最大、最小长度,取值范围以及是否为必填项等;
- 正常场景和异常场景的返回值;
- 参数之间的关联关系说明。
依据接口文档可以开始设计测试用例了。从测试场景来看,测试用例通常包括以下几种。
(1)验证正常业务流程
使用合法的入参构造测试场景,验证接口是否实现了业务功能。需要检查返回的数据、状态码是否正确,返回数据的数值精度是否足够,空字符串有没有被返回成NULL,数字枚举值是否映射到有业务含义的字符串上。测试时,为了验证业务处理流程的准确性,建议和上游服务进行联调,这样做方便验证上游服务在传递数据格式时能够按照约定来完成,也能提前发现联调类的缺陷。同时,还要发送数据给下游服务,检验数据是否能被正常消费。
(2)验证非法参数的返回值
验证使用非法参数时,接口的返回值是否符合期望。对于异常场景,我们基于边界值、等价类划分等方式设计测试用例,通过以往测试经验和测试理论扩散出各种异常场景。例如:对必填项、非必填项进行测试,对参数为NULL、超长字符串、超出允许值范围的数和枚举值、空字符串、非法日期格式、其他字符集数据(例如阿拉伯文)、特殊字符等场景进行测试。若接口在异常场景直接返回Internal Error的错误提示,没有任何详细的错误信息,这是不符合接口设计要求的。我们期望对所有异常场景都能给出明确的错误原因和代码。
在微服务测试中还出现过这样一种场景,举例来说,上游服务的开发人员口头承诺交易币种永远不会发生改变。对这种情况的处理方式是,在相关下游服务中对这个字段加上校验,确保币种发生改变时,能够直接抛出错误代码,这样可以避免相关下游服务中关联数据出错。
(3)验证接口的幂等性
熟悉微服务分布式特点的测试者都明白,在网络不稳定的情况下,可能会出现重复调用同一接口的情况。这时,要确保在使用相同参数、连续重复调用的情况下,最终完成的效果和调用一次时相同。
(4)验证分布式事务的处理
对微服务系统进行服务划分时,原则上尽量避免使用分布式事务,然而不可避免地还会出现多个服务协作处理事务的场景,因此模拟各种异常场景验证分布式事务是否正确,也是必不可少的。
(5)验证前后端校验逻辑的一致性
前后端分离的系统中,在前端页面和后端接口都会做数据校验,这两套校验逻辑应是一致的,不可以出现前端校验通过、后端返回错误结果的情形。
(6)验证并发线程的调用
在接口的实现代码中,如果线程安全没处理好,就会出现多个线程共同修改同一份内存变量的现象,造成数据写入错误,通常使用压测工具来对并发线程的调用进行测试。
(7)性能
数据库表中的数据量、关联表的个数等对接口的响应时间有直接影响,在确定性能测试场景时,要明确铺底的测试数据量以及期望的接口返回时间。对微服务的性能测试,本书第5章会进行详细的讨论。
常用的测试HTTP接口的工具有Postman、SoapUI、JMeter等。测试方法很简单,在工具中设置好请求地址、Header和Cookie等信息,以及请求类型和Body内容,即可对接口进行测试,如图3-2所示。
图3-2 用Postman做接口测试
3.1.2 接口测试重点
在微服务的接口测试中,这几个测试重点需要特别关注:幂等性、并发线程的调用以及分布式事务处理。
1. 幂等性
在接口测试中,幂等性是指同一个请求发送一次和发送多次,对服务器资源改变的效果是相同的。
典型场景如下。
- 一笔扣款请求遇到网络故障后引发重试,但无论重试了多少次,用户账户的钱只能被扣除1次。
- 用户在页面表单上多次点击鼠标提交数据,期待程序运行的结果是后台新增记录为1条而不是多条。
通常GET方法在设计上是需要幂等的。这里的幂等,不是发送GET请求的每一次返回结果完全相同,而是“多次发送GET请求”这个事件,不会引起服务器端资源的改变。例如,查询投票人数最高的10个帖子的请求,可能每次请求返回的帖子列表都不同,但由于没有改变服务器端的资源,因此接口是符合幂等性的。
POST请求一般会修改服务器端的资源,但并不代表它对应的接口一定不符合幂等性。如何判断POST接口具有幂等性?主要看使用相同参数重复调用POST接口后,后台系统经过判断是否允许插入同样数据到数据库。如果经过多次调用,对资源的修改却只发生了一次,那么该接口也是具有幂等性的。
幂等性是分布式系统中很重要的概念,微服务系统中有很多远程接口调用,当出现超时、网络异常等情况时,服务可能会多次发送同一个请求给接口,如果接口不具备幂等性,就会导致系统发生错误。幂等性的测试主要针对写操作的接口,对于有资金交易的业务场景,一定要测试接口的幂等性。开发人员为了保证接口幂等性,会对接口的入参做判断,有时使用序列号,有时使用多个字段联合作为判断主键的方式。
验证接口的幂等性可以使用Postman等接口测试工具。开始测试之前,我们需要了解接口的业务处理内容,接口对服务器端资源会做什么样的改变,例如写入数据库、写入缓存、更新服务器端文件等,这些都可以作为幂等性测试中的检查点。当后端接口的幂等性有保障时,在前端页面上即使用户重复多次点击提交按钮,接口也不会出现幂等性的问题,当然,前端通常也会有相应措施,来阻止用户触发重复事件。
2. 并发线程的调用
接口的实现代码如果没有处理好线程安全问题,会出现多个线程共同修改一份内存变量的现象,从而引发缺陷。通常来说在单线程中正常运行的代码,在多线程环境中可能会出现以下问题:
1)多线程并发访问时,插入数据库中的业务主键发生重复的现象;
2)加锁、解锁方式不当导致多线程调用时出现死锁;
3)多线程共同修改共享的内存变量,彼此修改掉对方数据,造成最终数据错误。
为了防止多线程调用引发的缺陷,开发工程师通常会使用锁机制,确保多线程并发时只有一个线程能获得锁,获得锁的线程在访问资源进行业务处理时,其他线程处于等待状态,该线程执行完毕并释放掉锁后,其他线程再依次执行。
对多线程并发的测试,使用压测工具比较方便。在工具中设置多线程并发执行的集合点,让多个线程同时调用接口,对写入数据库的数据进行验证。举个例子,如图3-3所示,在JMeter中设置40个线程,1个集合点。集合点的作用是线程启动后并不立即调用接口,而是在这40个线程全部启动完毕后,再统一开始调用接口,这样可以模拟并发线程的场景。JMeter设置40个线程所期望的结果是:因为40条数据的业务主键相同,1条会插入成功,39条会失败。如果后台代码线程安全做得不好,就会出现2~5条插入数据库成功的现象,这就是一个接口并发场景的缺陷。此外,测试过程中多线程并发调用时,可能出现死锁现象,大量线程处于执行中,长时间无法返回结果。在使用工具测试的过程中,可以为每个请求的一组入参添加同一个随机的字符串后缀,以区分不同线程发送的请求数据,测试执行完毕后,再去数据库检查有没有被其他线程篡改的情况。
图3-3 JMeter中的集合点设置
3. 分布式事务
应用程序中的事务在一系列业务操作后,会呈现出完成提交或全部撤销两种状态中的一种。例如,用户在网上提交订单,后台系统一边调用订单生成数据,一边调用库存服务扣减库存。这两步同时完成则事务成功,其中一步失败,则两步操作都要回滚。如果在微服务系统中下单过程涉及了两个服务,它们有各自的数据库,则需要分布式事务来保障下单过程的事务一致性。
当系统只使用了一个数据库时,可以借助数据库的事务处理能力来保证事务一致性。关系型数据库在事务处理方面的技术比较成熟,当遇到断电后重启这类情况时,数据库会先读取日志文件进行一系列恢复操作。在微服务系统中,涉及多个服务的跨数据源的事务,属于分布式事务范畴。如何应对网络不稳定、服务永久宕机等网络故障对事务的影响,是处理分布式事务的难点。目前业界的分布式事务方案没有一个能够完美到适用于所有业务场景,任何一个方案都需要在性能、一致性、系统可用性等方面做权衡和取舍。
现有的微服务系统分布式事务解决方案主要有:两阶段提交、TCC(Try-Confirm-Cancel)、可靠消息最终一致性、尽最大努力通知。
在进入每个方案之前,先了解一下这些方案所基于的CAP理论。
CAP(Consistency、Availability、Partition tolerance,即一致性、可用性、分区容错性)理论是分布式事务的重要理论支撑,其中分区容错性为分布式系统天然自带特性,表示多个节点中的一个节点不可用,不影响其他节点对外提供服务。同时CAP理论告诉我们,一个分布式事务最多只能同时满足其中两个特性。那么面对3个特性,该做怎样的取舍呢?
首先,在需要保证强一致性事务的情况时,追求一致性和分区容错性。例如银行转账系统。虽然更多的微服务系统在设计时追求的是可用性和分区容错性,可实际上也没有放弃一致性,会保证在一定时间内数据最终达到一致。强一致性是指任何一次读操作提取的都是最近一次写操作写入的数据,同时要保障在任意时刻所有节点中的数据是相同的。最终一致性是指不保证在任意时刻任意节点上的同一份数据都是相同的,而是这些节点间的数据在最终达到一致的状态。
测试分布式事务可以从项目使用的分布式处理方案特点入手,以此来分析测试工作的关注点。
(1)两阶段提交
传统的两阶段提交的方案中,存在协调者和参与者两个角色,参与者一般由数据库“担任”。执行事务处理有两个阶段:第一阶段为准备阶段,协调者发送消息,让每个参与者分别在本地执行自己的事务操作,写入Undo/Redo日志,并把操作结果返回给协调者;第二阶段为提交阶段,如果所有参与者都返回成功,则协调者再次发送指令,让所有参与者做提交操作。如果有任何一个参与者返回失败,协调者会发送指令让所有的参与者做回滚操作,参与者会根据本地记录的日志做一系列的撤销操作,让系统回到事务开始前的状态。
两阶段提交可以实现强一致性的事务,不适用于高并发及参与者事务执行周期长的业务。目前阿里巴巴的开源项目Seata支持两阶段提交的分布式事务。
测试关注点如下:
- 每个参与者的事务都成功的场景;
- 某个参与者事务失败,其他参与者事务均可以成功回滚的场景;
- 某个参与者在处理事务的过程中出现网络超时,其他参与者的事务做回滚处理的场景。
(2)TCC
TCC方案对分布式事务处理有3个步骤:Try、Confirm和Cancel。在这个方案中,如果参与者执行事务操作在Try阶段成功,则默认其在Confirm阶段一定成功。如果参与者在Try阶段执行失败,则会对其他在Try阶段执行成功的参与者执行回滚操作。如果某一个参与者在Confirm阶段执行失败,则进行重试或者人工介入。TCC方案默认参与者事务在Cancel阶段一定能执行成功,如果失败,重试或者人工介入。
在TCC的方案中,分布式事务的参与者要对业务实现Try、Confirm和Cancel三个接口,代码侵入性较高,实现也较复杂。TCC方案的性能优于两阶段提交方案,可以实现分布式事务最终一致性。
实现TCC方案的框架有ByteTCC、Hmily、TCC-transaction等。
测试关注点如下:
- 每个参与者的事务都成功的场景;
- 某个参与者事务失败,其他参与者的事务可以回滚成功的场景;
- 某个参与者在处理事务的过程中出现网络超时,其他参与者的事务做回滚处理的场景;
- 因为Confirm或者Cancel接口调用失败会进行重试,所以要验证接口是否符合幂等性;
- 出错后是否可以通过短信、邮件等方式让人工介入处理。
(3)可靠消息最终一致性
该方案的思路是,当事务发起方执行完本地事务,发送一条消息给参与者后,参与者一定能接收到消息并处理事务成功。
目前能实现这一方案的有本地消息表和RocketMQ(阿里巴巴的分布式消息中间件)事务消息等方法。后者一般会使用MQ消息中间件作为消息传递渠道,并依靠MQ的ACK机制(即消息确认)确认消息是否被消费掉了。
可靠消息最终一致性方案,适合对吞吐量要求高于对实时性要求的场景。
测试关注点:
- 本地事务和消息发送是否具有原子性;
- 事务的接收方出现故障不可用,恢复后是否可以重新接收消息;
- 由于接口有多次消费同一个消息的场景,需要验证接口的幂等性。
(4)尽最大努力通知
这一方案对时间的敏感性最低,一般适用于交易后的通知。发起方把业务结果发给被通知方,当被通知方接收不到消息时,需要被通知方主动调用发起方的接口查询业务处理结果。
测试关注点如下:
- 发起方处理完成对应业务并发送消息后,验证被通知方接收到的消息中是否包含所需的业务完成后的信息;
- 发起方处理完成对应业务却没有发送消息,则验证被通知方是否可以使用查询接口查询到业务完成后的相关信息。