编辑 | 邵一帆
收钱吧业务服务千万级商家,业务庞大,产品背后有复杂的应用支撑。我们采用了微服务架构,有成百上千个不同类型的后端服务,使用了包括Node.js、Java、Go、Python等后端语言,还有Mysql、MongoDB等数据库以及Elasticsearch、Kafka、Redis、Apollo、RabbitMQ等中间件。
随着产品需求复杂性的不断上升,传统功能测试的片面性及滞后性导致测试成本急剧增加、测试效率大幅度下降,仅靠功能测试已难以持续保障项目质量及交付效率。
而自动化测试可以帮忙测试人员在项目初期就能发现系统深层次的问题,并且降低了问题修复的时间成本,其好处显而易见。自动化测试也在各大互联网公司逐步地落地,这是大势所趋,收钱吧质量工程部在这几年里一直在践行高效的自动化测试,并且有了一些成果。
自动化测试是一个泛称,它包含了诸如单元测试、接口测试、Web 测试等具体测试手段,每个自动化测试手段有其优劣势,找到适合收钱吧技术状况的自动化策略是能够实践成功的第一步。
在微服务架构下,相比测试金字塔 [1],我们更加推崇蜂窝型分层 [2]。
这是因为:
接口自动化测试兼顾了介入早、维护成本低、业务逻辑覆盖完整等优点,因此它成了我们自动化测试重点投入的方向。
除了明确 接口测试是重点以外,我们还要基于微服务架构的分层特点,进一步细化自动化测试策略。
我们简明扼要的描述下当前的系统架构,如图:
成百上千个后端服务有不同的分层界定,我们可以从接入方调用的视角简化成下图所示:
基于以上分析,我们了解了服务调用链路以及每层服务的主要职责。然后可以根据每层服务的侧重点,进行策略细分:
在测试策略清晰明确的前提下,我们就可以着手进行自动化用例的开发工作。
与大部分公司类似,我们采用了Python作为开发语言,使用内置的unittest [3]单元测试框架组织测试用例,采取pytest [4]执行测试用例。
我们测试框架具备的能力大致如下:
本章提到的接口功能测试是针对某一服务端的单个接口的功能测试。在测试过程中,我们需要关注以下几点:
与此同时,要想达到高效且高覆盖的测试目标,测试人员需要从用户角度出发并结合开发代码的实现,抽取出有效的等价类进行接口功能测试,如此可以减少大量时间成本,不做过度或者无效的测试。下面举一个例子来说明:
如何高效地测试一个登陆接口(用户名、密码)
与大部分人设计的用例不同,针对登陆功能,我们只设计2条用例覆盖,而不考虑用户名、密码长了短了、是否包含无效字符等各种错误条件的排列组合:
之所以这样设计,是因为我们提前了解到接口的实现:只是去查询数据库中否存在匹配的用户名和密码,即能够匹配成功的只有一组字符串,而用所有非法的字符串集合去数据库查询都会返回失败,这一类输入都属于异常等价类。因此我们无需去穷举入参进行测试。
def test_login_succ(self):
res = self.client.login(username, password)
self.assertEqual(res.code, SUCC)
在单接口功能测试保障的前提下,我们需要保障接口之间交互以及数据流转的正确性。使用多服务的多接口之间的调用,就可以串联业务场景覆盖,这是功能测试覆盖最重要的手段。以下是集成测试用例设计关键点列举:
举例说明,通过接口集成测试来进行测试:
新增商户报名支付源活动,费率优惠支付
测试设计:
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)
收钱吧的业务牵涉到资金流动,如支付、分账、分润、代付等等。如果程序控制不当,就会造成严重的资金损失。引起资损的原因有很多,例如用户重复提交、程序并发问题、业务逻辑过程处理不当、金额换算处理不正确、安全漏洞等等。根据不同的业务场景,开发会选择不同的实现方案来解决问题。如唯一索引约束、分布式锁、数据库排他锁等。即便如此,测试人员仍需要进行资损相关的测试,从而保障产品质量。下面列举了三种场景,详细说明如何进行资损测试。
当调用方重复发起同一笔请求,系统需要做幂等处理,确保只能扣款一次
要验证这个功能,就需要借助自动化测试的能力:模拟同一个用户顺序发起多次同样支付请求、期望只成功一次。
代码样例如下:
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分钱
当调用方并发多次同一笔支付请求,系统要确保只能扣款一次
与上一个场景的区别在于:调用请求不再友善地挨个发起,而是一哄而上,这个时候就需要用多线程模拟同用户并发调用。
代码样例如下:
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分钱
对同一账户同时发起多次储值服务、核销、退款、多次请求可以成功多次
测试用例设计:
代码样例如下:
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
在我们系统中,很多业务依赖三方接口,而我们无法依赖对方的测试环境来验证我们自身的逻辑,尤其是对依赖接口异常的处理。我们自研了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)
我们自研了Zepar——通用的自动化测试执行平台,来解决测试用例呈现、测试计划管理、自动/手动执行的需求。通过平台化管理,大大提高了自动化用例的使用率,而且也方便其他部门的同事应用我们的自动化测试能力。
测试计划管理 执行记录另外我们引入了开源的测试报告平台:ReportPortal [5]。它是一个自动化测试用例日志收集、结果分析的可视化平台,可以与主流测试框架集成,如TestNG、Pytest、Junit、Nunit、SoapUI等。与此同时,该平台可以多维度统计报表,支持在线分析测试结果,并可以展示出美观的分析报告,如下图所示。
利用ReportPortal,我们强化了失败用例的分析,也能够帮助发现不稳定用例 [6],及时修复,防止测试用例的腐化。
代码在日常的迭代中,往往会呈现出自然地腐化,从刚开始清晰明了的架构慢慢演变成四不像,越来越难维护,自动化测试用例同样避免不了陷入这种尴尬的境地。
在代码层面,我们要求遵循PEP8 [7]的代码风格,用例设计上要遵循AIR [8]原则,在Function的docstring [9]中写清楚测试用例的每一个步骤,并且每次代码提交都需要进行Code Review。在框架中,我们大量利用装饰器 [10]来隐藏一些技术细节,让测试工程师尽可能只关注业务逻辑,这样可以更容易的编写自动化用例。我们鼓励写出Pythonic [11]的代码,对低质量的代码坚决Say No。
我们没有 专职的自动化测试工程师,也就是说在收钱吧的测试工程师,你既要做业务测试又要维护自动化用例:)。职责清晰,责任界定也就明确,每一个测试工程师都要对他负责业务对应的自动化用例可用性负责,没有什么可推诿的。
以上是在收钱吧项目测试中积累的有关自动化测试的一些实践与成果。目前,我们的接口测试用例 总数达到30000以上,可用率**超过95% ,核心服务代码覆盖率更是超过70%**。我们还对测试效率提出了更高的要求:随着自动化用例数量的不断增加,技术实现中出现的大量异步场景,自动化执行耗时正在增长。针对这些痛点,我们正进一步研发分布式执行框架,一劳永逸地解决这些问题。
在当下的敏捷时代,行业要求速度和质量。自动化测试的兴起无外乎成为了行业中的宝贵资源。当严谨的测试方案、完善的测试用例,遇上了高效的自动化测试手段,便成为了提升测试效率和产品质量的一把利器,不仅保证高效的回归测试,缩减大量手工测试,同时将接口用例输出给开发自测回归,给开发增添上线信心。我们将在自动化测试这条道路上不断探索、保持热忱、持续优化,为我们产品的质量保驾护航。
任斌,来自质量工程部
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