收钱吧的 Python 高效自动化测试实践
1. 背景
收钱吧业务服务千万级商家,业务庞大,产品背后有复杂的应用支撑。我们采用了微服务架构,有成百上千个不同类型的后端服务,使用了包括Node.js、Java、Go、Python等后端语言,还有Mysql、MongoDB等数据库以及Elasticsearch、Kafka、Redis、Apollo、RabbitMQ等中间件。
随着产品需求复杂性的不断上升,传统功能测试的片面性及滞后性导致测试成本急剧增加、测试效率大幅度下降,仅靠功能测试已难以持续保障项目质量及交付效率。
而自动化测试可以帮忙测试人员在项目初期就能发现系统深层次的问题,并且降低了问题修复的时间成本,其好处显而易见。自动化测试也在各大互联网公司逐步地落地,这是大势所趋,收钱吧质量工程部在这几年里一直在践行高效的自动化测试,并且有了一些成果。
2. 测试策略
自动化测试是一个泛称,它包含了诸如单元测试、接口测试、Web 测试等具体测试手段,每个自动化测试手段有其优劣势,找到适合收钱吧技术状况的自动化策略是能够实践成功的第一步。
在微服务架构下,相比测试金字塔[1],我们更加推崇蜂窝型分层[2]。
这是因为:
对于微服务模型的项目来说,服务即『单元』,接口表达了『单元』的能力,并实现了单元之间的通讯,编排接口的调用则完成了业务、产品逻辑; 单元测试代码量大、开发工程师无法投入较多的精力进行维护,且不能够通过单元测试来实现服务之间的集成测试、覆盖业务场景,实际收益其实并不明显; 在项目初期有接口定义的情况下,测试工程师可以较早地介入项目当中设计、开发接口测试用例,无需等开发工作全部完成,实现测试左移,可以更早地发现系统问题,提高了整体的效率。
接口自动化测试兼顾了介入早、维护成本低、业务逻辑覆盖完整等优点,因此它成了我们自动化测试重点投入的方向。
2.1 细化微服务下的测试策略
除了明确接口测试是重点以外,我们还要基于微服务架构的分层特点,进一步细化自动化测试策略。
我们简明扼要的描述下当前的系统架构,如图:
接入层:面向客户端,客户端通过调用接入层的接口来请求应用层的服务,得到响应后,包装数据返回,给客户端使用。比如API网关,它的主要职责包含请求认证鉴权、安全校验、返回值的包装、请求的转发,其本身并没有业务逻辑; 应用层:服务面向业务,它通过对一个或者多个领域层服务的调用、编排来实现业务功能。例如我们系统中的业务开通服务,它依赖用户中心、商户中心、进件、银行卡等领域层的服务来实现其功能; 领域层:面向领域对象,领域对象是具有高内聚、低耦合的特点,且具有单一的职责。例如我们系统中的支付服务、银行卡服务、分账服务等,它们只实现本身的业务规则和逻辑,可能会依赖一些三方服务、内部服务; 基础设施层:如关系型数据库、缓存服务、消息队列等。
成百上千个后端服务有不同的分层界定,我们可以从接入方调用的视角简化成下图所示:
基于以上分析,我们了解了服务调用链路以及每层服务的主要职责。然后可以根据每层服务的侧重点,进行策略细分:
接入层:重点关注接口鉴权、安全校验、输入输出合法性、接口联通性、路由转发的正确性; 应用层:重点需要根据接口的业务逻辑做到单接口的功能覆盖,且从业务场景考虑,串联组装该层接口去覆盖业务流程,从而达成集成测试的目的; 领域层:重点关注领域对象的业务规则、算法、与三方交互的逻辑,从而做到接口功能覆盖。
3. 自动化测试实践
在测试策略清晰明确的前提下,我们就可以着手进行自动化用例的开发工作。
3.1 技术选型
与大部分公司类似,我们采用了Python作为开发语言,使用内置的unittest[3]单元测试框架组织测试用例,采取pytest[4]执行测试用例。
我们测试框架具备的能力大致如下:
测试数据对象化管理、关联关系管理 支持数据驱动 多维度分拣测试用例能力,可以方便组织不同的测试计划 多环境执行能力 中间件连接的池化 自动报告集成 平台集成
3.2 接口功能测试
本章提到的接口功能测试是针对某一服务端的单个接口的功能测试。在测试过程中,我们需要关注以下几点:
参数校验:参数的必选校验、参数组合校验、参数类型是否合法校验、取值的边界校验; 接口权限校验:校验接口的使用权限是否存在水平、垂直越权; 返回值校验:校验返回值是否符合预期,包括返回格式、响应码、消息体、错误文案、返回数据是否正确; 持久化校验:落地数据的正确性校验; 中间件校验:缓存、消息存储逻辑校验; 接口逻辑验证:接口内部业务逻辑尽量做到分支覆盖。
与此同时,要想达到高效且高覆盖的测试目标,测试人员需要从用户角度出发并结合开发代码的实现,抽取出有效的等价类进行接口功能测试,如此可以减少大量时间成本,不做过度或者无效的测试。下面举一个例子来说明:
如何高效地测试一个登陆接口(用户名、密码)
与大部分人设计的用例不同,针对登陆功能,我们只设计2条用例覆盖,而不考虑用户名、密码长了短了、是否包含无效字符等各种错误条件的排列组合:
用例1:使用正确的用户名,密码登陆 用例2:使用错误的用户名或密码登陆
之所以这样设计,是因为我们提前了解到接口的实现:只是去查询数据库中否存在匹配的用户名和密码,即能够匹配成功的只有一组字符串,而用所有非法的字符串集合去数据库查询都会返回失败,这一类输入都属于异常等价类。因此我们无需去穷举入参进行测试。
def test_login_succ(self):
res = self.client.login(username, password)
self.assertEqual(res.code, SUCC)
3.3 接口集成测试
在单接口功能测试保障的前提下,我们需要保障接口之间交互以及数据流转的正确性。使用多服务的多接口之间的调用,就可以串联业务场景覆盖,这是功能测试覆盖最重要的手段。以下是集成测试用例设计关键点列举:
业务正常流程覆盖:通过各服务的接口调用来组装正常流程上的用例,保证业务流程上各分支的正常流程都被覆盖到; 业务异常流程覆盖:通过调用接口来覆盖业务流程上的异常分支,模拟业务节点返回异常,来测试系统的容错性,以及出错后的后续的业务补偿流程; 调用链路覆盖:保证系统中每条链路都覆盖到,从下游接口发起,覆盖整条链路; 接口时序覆盖:接口调用按照时序有效路径去设计,避免无效的路径。
举例说明,通过接口集成测试来进行测试:
新增商户报名支付源活动,费率优惠支付
测试设计:
正确的用例包含新增商户入网、活动报名、报名审批、触发支付源进件、费率生效、优惠支付成功等场景; 异常用例存在很多种情况,任一流程失败,都会导致最终业务失败;
def test_new_merchant_pay_success(self):
res = self.merchant_service.create_merchant()
self.assertEqual(res.code, SUCC)
another_res = self.another_service.do_something(res.field)
self.assertTrue(another_res, COMPLETE)
3.4 资损测试
收钱吧的业务牵涉到资金流动,如支付、分账、分润、代付等等。如果程序控制不当,就会造成严重的资金损失。引起资损的原因有很多,例如用户重复提交、程序并发问题、业务逻辑过程处理不当、金额换算处理不正确、安全漏洞等等。根据不同的业务场景,开发会选择不同的实现方案来解决问题。如唯一索引约束、分布式锁、数据库排他锁等。即便如此,测试人员仍需要进行资损相关的测试,从而保障产品质量。下面列举了三种场景,详细说明如何进行资损测试。
3.4.1 重复调用
当调用方重复发起同一笔请求,系统需要做幂等处理,确保只能扣款一次
要验证这个功能,就需要借助自动化测试的能力:模拟同一个用户顺序发起多次同样支付请求、期望只成功一次。
代码样例如下:
def test_pay_more_time():
old_balance = self.client.get_merchant_balance(merchant_id) # 获取商户余额
[self.client.pay(amount, client_sn) for _ in range(5)] # 多次发起支付
current_balance = self.client.get_merchant_balance(merchant_id) # 重新获取余额
assert current_balance == old_balance + amount # 断言商户余额只增加了1分钱
3.4.2 并发请求同一接口
当调用方并发多次同一笔支付请求,系统要确保只能扣款一次
与上一个场景的区别在于:调用请求不再友善地挨个发起,而是一哄而上,这个时候就需要用多线程模拟同用户并发调用。
代码样例如下:
def test_pay_concurrency(self):
old_balance = self.client.get_merchant_balance(merchant_id) # 获取商户余额
pool = [threading.Thread(target=self.client.pay, args=(1,)) for _ in range(10)]
[t.start() for t in pool]
[t.join() for t in pool] # 使用多线程并发调用支付请求,金额为1分钱,并发n次
current_balance = self.client.get_merchant_balance(merchant_id)
assert current_balance == old_balance + 1 # 断言商户余额只增加了1分钱
3.4.3 多种资金流动接口并发调用
对同一账户同时发起多次储值服务、核销、退款、多次请求可以成功多次
测试用例设计:
对同一个账户不同的操作进行并发调用; 并发后结果校验需满足:账户余额变化 = 累计充值金额 - 累计次核销总金额 - 累计退款金额;
代码样例如下:
def test_account()
old_balance = self.client.get_merchant_balance(merchant_id) # 获取商户余额
concurrent_add(self.client, add_amount, a) # 并发储值
concurrent_reduce(self.client, reduce_amount, b) # 并发核销
concurrent_refund(self.client, refund_amount, c) # 并发退款
current_balance = self.client.get_merchant_balance(merchant_id)
assert current_balance == old_balance + a*add_amount-b*reduce_amount-c*refund_amount
3.5 Mock第三方接口测试
在我们系统中,很多业务依赖三方接口,而我们无法依赖对方的测试环境来验证我们自身的逻辑,尤其是对依赖接口异常的处理。我们自研了Mock Server,这类场景的测试因此就不依赖第三方的环境。通过Mock Server控制响应异常,可以实现各种异常场景测试。比如:要模拟进件失败,支付时银行卡冻结等。
Mock Server采用高性能响应的go语言框架,而且可以动态调整返回的结果,可以很方便的集成到自动化测试中。如:
@mock("RETURN_CODE", "ACCOUNT_ERROR")
def test_pay_abnormal():
response = self.client.pay()
self.assertEqual(response.code, FAIL)
4. 平台化管理
4.1 自动化执行平台
我们自研了Zepar——通用的自动化测试执行平台,来解决测试用例呈现、测试计划管理、自动/手动执行的需求。通过平台化管理,大大提高了自动化用例的使用率,而且也方便其他部门的同事应用我们的自动化测试能力。
4.2 报告平台
另外我们引入了开源的测试报告平台:ReportPortal[5]。它是一个自动化测试用例日志收集、结果分析的可视化平台,可以与主流测试框架集成,如TestNG、Pytest、Junit、Nunit、SoapUI等。与此同时,该平台可以多维度统计报表,支持在线分析测试结果,并可以展示出美观的分析报告,如下图所示。
利用ReportPortal,我们强化了失败用例的分析,也能够帮助发现不稳定用例[6],及时修复,防止测试用例的腐化。
5. 防腐化措施
代码在日常的迭代中,往往会呈现出自然地腐化,从刚开始清晰明了的架构慢慢演变成四不像,越来越难维护,自动化测试用例同样避免不了陷入这种尴尬的境地。
在代码层面,我们要求遵循PEP8[7]的代码风格,用例设计上要遵循AIR[8]原则,在Function的docstring[9]中写清楚测试用例的每一个步骤,并且每次代码提交都需要进行Code Review。
在框架中,我们大量利用装饰器[10]来隐藏一些技术细节,让测试工程师尽可能只关注业务逻辑,这样可以更容易的编写自动化用例。我们鼓励写出Pythonic[11]的代码,对低质量的代码坚决Say No。
我们没有专职的自动化测试工程师,也就是说在收钱吧的测试工程师,你既要做业务测试又要维护自动化用例:)。职责清晰,责任界定也就明确,每一个测试工程师都要对他负责业务对应的自动化用例可用性负责,没有什么可推诿的。
6. 总结与展望
以上是在收钱吧项目测试中积累的有关自动化测试的一些实践与成果。目前,我们的接口测试用例总数达到30000以上,可用率超过95%,核心服务代码覆盖率更是超过70%。我们还对测试效率提出了更高的要求:随着自动化用例数量的不断增加,技术实现中出现的大量异步场景,自动化执行耗时正在增长。针对这些痛点,我们正进一步研发分布式执行框架,一劳永逸地解决这些问题。
在当下的敏捷时代,行业要求速度和质量。自动化测试的兴起无外乎成为了行业中的宝贵资源。当严谨的测试方案、完善的测试用例,遇上了高效的自动化测试手段,便成为了提升测试效率和产品质量的一把利器,不仅保证高效的回归测试,缩减大量手工测试,同时将接口用例输出给开发自测回归,给开发增添上线信心。我们将在自动化测试这条道路上不断探索、保持热忱、持续优化,为我们产品的质量保驾护航。
7. 关于作者
参考资料
Test Pyramid: https://martinfowler.com/bliki/TestPyramid.html
[2]Testing of Microservices: https://engineering.atspotify.com/2018/01/testing-of-microservices/
[3]Unit testing framework: https://docs.python.org/3/library/unittest.html
[4]pytest: helps you write better programs: https://pytest.io
[5]Reporting automated testing analytics machine learning : https://reportportal.io
[6]Test Flakiness – Methods for identifying and dealing with flaky tests: https://engineering.atspotify.com/2019/11/test-flakiness-methods-for-identifying-and-dealing-with-flaky-tests/
[7]PEP 8 – Style Guide for Python Code: https://peps.python.org/pep-0008/
[8]单元测试 AIR 原则: https://juejin.cn/post/6844903923447250957
[9]PEP 257 – Docstring Conventions : https://peps.python.org/pep-0257/
[10]PEP 318 – Decorators for Functions and Methods : https://peps.python.org/pep-0318/
[11]What is Pythonic?: https://www.computerhope.com/jargon/p/pythonic.htm#:~:text=Pythonic%20is%20an%20adjective%20that,way%20is%20called%20%22pythonic.%22
还不过瘾?试试它们