1 简介
51 信用卡管家自 2015 年开始实施微服务架构,是业界较早尝试微服务架构的技术团队,整个团队有幸见证了微服务从最初的个服务试点到全面铺开的过程。架构的演变也催生了自动化测试框架和策略的演变,测试团队通过持续地探索和总结,在集成测试自动化框架建设和策略选择上积累了一些经验,抛砖引玉和大家一起分享。
2 微服务架构下集成测试自动化的困境
2.1 微服务架构给测试带来的改变
先看下《微服务设计》1.3 章节对 SOA 的定义:
SOA(Service-Oriented Architecture,面向服务的架构)是一种设计方法,其中包含多个服务,而服务之间通过配合最终会提供一系列的功能。一个服务通常以独立的形式存在于操作系统进程中。服务之间通过网络调用,而非采用进程内调用的方式进行通信。
微服务架构是 SOA 的一种特定方法,基于这种架构在开发层面带来的好处在《微服务设计》一书中描述得很清楚了。从测试角度来看,通信方式由进程内调用变成网络调用,最明显的改变有两个:
第一,数据流转更加清晰。微服务架构下的时序图非常清晰,服务之间分工明确,代替了传统服务数据只在服务内部模块间流转的方式。
第二,数据入口多元化,可测试性增强。服务的拆分带来的另一个好处就是测试粒度变小变细,每个微服务都具备独立的网络调用方式,不管是提供 Http 接口还是消费消息队列,都给集成测试可测试性和覆盖率带来了便利和提升。
2.2 集成测试自动化的困境
软件工程没有银弹,对于微服务也是一样。在《微服务设计》1.1.1 章节作者 Sam Newman 对微服务架构的担忧:
当考虑(微服务)多小才足够小的时候,我会考虑这些因素:服务越小,微服务架构的优点和缺点也就越明显。使用的服务越小,独立性带来的好处就越多。但是管理大量服务也会越复杂,本书的剩余部分会详细讨论这一复杂性。如果你能够更好地处理这一复杂性,那么就可以尽情地使用微服务了。
一方面服务拆分给测试带来很多便利,另一方面服务数量的增加也带来了测试复杂度的指数增长,好像陷入了一种囚徒困境。
以图 2.2.1 的功能为例,该功能一共涉及了 7 个微服务,2 个消息中间件,还有没在时序图上体现的两种 DB(mysql/cassandra)和缓存中间件(redis)。由于服务和中间件之间的调用也是网络调用,所以在测试过程可以把中间件和 DB 当成一个微服务节点来分析。面对这样一个涉及 12 个微服务的功能(不考虑集测自动化策略,比如拆分成几个小功能分块实现自动化降低复杂度),我们需要一个什么样的框架来支撑,才能让测试工程师写出的集测自动化 case 具备易用性、可维护性这些美好的特性。
2.2.1 功能 A 服务调用时序图
2.3 拆解困境
当时 51 信用卡的集测框架大概使用了一年多,中间迭代过两个大的版本,已经从最原始的纯 Java 框架过渡到基于关键字驱动和数据驱动开发的框架,原理参考图 2.3.1。
图 2.3.1
初代框架第一层是测试脚本(参考下图的代码)和 Java Bean,第三层是工具层,由常用的 Util 类组成,比如:MysqlClientUtil、HttpClientUtil、RedisClientUtil 等。第二层是解析层或者执行层,它会将第一层的测试脚本初始化为对应的 Java Bean 实例,然后再把实例拆分成各个 Util 类的静态方法执行。
初代框架在面临图 2.2.1 复杂的功能时,最重要的一个问题就是扩展性。脚本的每个 modul 都代表了一次网络调用,可读性很好。但是测试流程被固化在每个 Java Bean 之中,每一个新的功能都有可能要定制一个新的 Java Bean(调整 action 调用顺序)和 DataProvider(图 2.3.1 蓝色部分)。而当 Util 类变更时,同样需要变更 Java Bean 和 DataProvider,这个变更的工作量堪称地狱级。
第二个问题是数据的重用。图 2.3.2 中的 userid 这个字段在脚本中出现 3 次,在某些复杂场景下可能就不是一个字段而是整个数据结构,降低了 case 的可维护性。数据库连接和中间件连接(脚本中的 redis client)每个 case 都会先初始化再销毁,这个对于执行效率也是一种影响。
基础框架有了,问题也找到了,下面我们要做的就是改造基础框架来满足新的自动化需求。
3 造一个“轮子”逃出困境
3.1 BDD&DSL
In software engineering, behavior-driven development (BDD) is a software development process that emerged from test-driven development(TDD).[1][2][3][vague] Behavior-driven development combines the general techniques and principles of TDD with ideas from domain-driven design and object-oriented analysis and design to provide software development and management teams with shared tools and a shared process to collaborate on software development.[1][4]
BDD is largely facilitated through the use of a simple domain-specific language (DSL) using natural language constructs (e.g., English-like sentences) that can express the behavior and the expected outcomes. Test scripts have long been a popular application of DSLs with varying degrees of sophistication. BDD is considered an effective technical practice especially when the “problem space” of the business problem to solve is complex.[5]
在寻找扩展性解决方案的过程中,了解到兄弟部门正在尝试用 Rest-Assured 代替初代框架中基于 Apache HttpClient 封装的 Util。通过了解 Rest-Assured 我们接触到了 BDD,其中领域驱动设计(domain-driven design)的 idea 让我们沉思:是不是初代框架在设计模式上就有缺陷?
从元数据角度看,BDD 中的 behavior 和关键字驱动是一个概念,关键字相对独立和分散,BDD 的优势是通过领域驱动对 behavior 做了一层抽象,并通过一定的逻辑(given when then)将 behavior 串联成一个具体的功能。这样带来的好处是,所有的 behavior 在当前领域只做一件事,比如在测试领域 behavior 就是做功能验证(或者为功能验证做准备),我们要做的就是针对测试领域的 behavior 抽象出一个父类,并通过一定规则将不同的 behavior 串联起来。
从 case 脚本的表现力来对比,数据驱动下的脚本只做数据存储,BDD 使用 DSL 作为脚本语言,可以表达出更丰富的行为和预期结果(express the behavior and the expected outcomes),可读性和信息的承载度上了一个台阶。这一 part 我们要做的就是选择合适的语言作为框架的 DSL。
通过对比 BDD 和关键字+数据驱动混合模式,我们得到了两个 action,后面要做的就是验证 action 是否正确。
3.2 行为父类 Behavior
测试的本质就是功能验证,所有的行为不是为了功能验证就是在为验证做准备。举个例子,测一个查询接口,首先在 DB 中插入需要的数据,然后调用查询接口,最后对比接口返回的实际数据是否和预期数据一致。在这个数据流转过程中,插入 DB 的数据可能有部分和接口返回的数据是同一个,手工执行的过程中,这些数据会存在测试工程的脑海或者中间媒介,相当于线程中的上下文,而每个行为都有可能去操作这个上下文。
针对上文测试行为的两个特征,抽象了两个接口 CompareIntf 和 AssignContextIntf。CompareIntf 负责功能验证,返回值就是预期值和实际值的对比结果,如果是为了验证做准备的行为,比如向 DB 插入数据,以行为的成功与否作为返回值。AssignContextIntf 负责和上下文进行数据交互,包括增删改查,而且只有当 CompareIntf 返回 true 的时候才会执行 AssignContextIntf。
最终抽象出的父类参考下文 UML 类图。Behavior 类成员变量中的两个 Map 以及相关的 api 先按下不表,下一章节会介绍其作用。className 的取值是子类的包名+类名,作用是通过类加载器以及双亲委派机制实例化子类,从而舍弃了初代框架中的固定的 Java Bean,将框架和具体行为的类名解耦,框架只负责流程的执行而不用关心流程中涉及哪些行为类,从而解决了扩展性的问题。remark 的作用是对 className 的应用场景做一个补充,比如调用某微服务查询接口,让每个行为的意图都保存在 case 中,增加可维护性。
图 3.2.1 框架类图
确定了父类之后,我们把初代框架第三层中的所有 Util 类都套上了一层装饰器(Behavior 的子类),主体逻辑在放在 CompareIntf 实现中。拿 web 开发类比的话,Behavior 类似 Controller,用来作为数据的入口,而 Util 类和 Service 类一样,负责功能的主体逻辑。
接着我们又把行为类进行分类,把功能类似的行为类放到同一个工程作为一个 Library,这样 Library 与框架,Library 与 Library 之间都完成了解耦。
解决了父类的抽象和扩展性问题,就差一个框架把所有的行为类整合成一条完整的自动化 case 了。
3.3 AutoTestFrame
图 3.3.1 自动化 case 执行流程图
在分析自动化用例执行流程时,我们发现和传统基于 BDD 开发的工具不一样的是,测试用例的执行流程 give-when-then 非常固定。参考图 3.3.1 左边流程图,把用例分割成三个部分数据准备-操作步骤-数据清理,首先执行数据准备,执行成功则执行操作步骤-数据清理,失败的话跳过操作步骤,直接执行数据清理,保证 case 不管成功失败对整个环境都是幂等操作。
用例的三个部分,不管是数据准备还是操作步骤,通常都不是单独的一个行为类,而是行为类的集合。我们把多个行为类组成一个 StepSet,数据准备和数据清理都是一个 StepSet,操作步骤有点特殊,会并行执行的多个 StepSet。
StepSet 的执行逻辑参考图 3.3.1 右边的流程图,串行执行行为类的 compareExpectAndActual 方法,返回 true 则执行下一个,返回 false 则 break 整个流程,case 执行失败。执行过程中行为类会和两个上下文发生数据交互,也就是上个章节行为类的两个 Map 成员。
基于以上的分析,我们在 DSL 中去掉了流程关键字(give-when-then),把流程控制硬编码在框架中。至此第一个 action 两 part 都解决了,框架的雏形接近完成,也该有个名字了,AutoTestFrame,简称 ATF。
3.4 DSL 选择
这一章没有前面那么多去伪存真的过程,ATF 还是沿用了初版框架中 Json 作为 DSL,而没有选择 BDD 推荐的自然语言。原因有三个:第一,Json 在 51 微服务的架构中具有天生的优势,服务间的通信使用 Http+Json 的 REST 规范,处于亲儿子的地位;第二,KV 类语言有着接近自然语言的表现力,除了 Json 还有 xml,yaml 等,初代框架的脚本在加上 remark 之后基本处于可读的状态;第三,Json to Java 有着丰富的第三方库,Jackson、FastJson、Gson 等,省去了编写和维护 DSL 解析器的精力。
既然是 DSL,在易读的同时也会有一些抽象的特性来简化操作。ATF 通过占位符配合关键字开发了一些常用的特性,比如时间函数(参考图 3.4.1)、Jpath、加解密等,这些 DSL 会在行为类初始化之前被替换为实际值。
图 3.4.1 时间函数 DSL
3.5 框架的应用
到此为止,ATF 通过引入 ClassName 和类加载器、规范 case 执行流程、确定 DSL 解决了扩展性问题,通过提取上下文并赋予行为类的上下文访问权限解决了数据重用问题,那开篇提到的问题有没有解决?实际在项目中使用的结果如何?
先来看下开篇提到的功能最终的脚本,参考图 3.5.1,最终我们把时序图拆成了三个部分来实现集测自动化,图中的脚本是从发送 kafka 消息开始一直到倒数第三个服务结束。脚本基本还原了时序图,具备不错的可读性,即使换一个完全不了解该业务的测试工程师,也能很快领会脚本的意图。至于 case 的 debug,和 jenkins 的打通由于篇幅有限,这里就不过多介绍了。
ATF 在 51 实施了一年多,有一个 Library 专门维护集测常用的 60 多个行为类,包括各种消息中间件、DB、Http 等。除此之外,我们也开发了和 Appium、Selenium 相关的 Library,还在试用阶段。不完全统计,目前已经使用 ATF 的项目超过 50 个,平均集测覆盖率超过 30%,核心业务的覆盖率超过 60%。
图 3.5.1 ATF case 脚本
3.6 RobotFramework
太阳底下没有新鲜事,我司经过三次迭代开发的集测自动化框架 ATF,并不是在自动化领域第一个吃螃蟹的。
Robot Framework is a Python-based, extensible keyword-driven test automation framework for end-to-end acceptance testing and acceptance-test-driven development (ATDD). It can be used for testing distributed, heterogeneous applications, where verification requires touching several technologies and interfaces.
RobotFramework 的 idea 在 2005 年出自 Pekka Klärck,同年在诺基亚开发出第一个版本,2008 年开源了 release2.0 版本,最近一次发版 V3.02 是在去年 7 月。目前官方提供超过 60 个 Libraries,不仅涉及集成测试领域,还包括 App、Web、DeskTop Client 等多个领域。虽然 RobotFramework 是用 Python 编写,但是可以通过一个扩展 LibraryJavalibCore 作为胶水来粘合 Java 编写的 Library。除了 DSL 外,RobotFramework 还有一个 DesckTop Client,支持通过 GUI 来编辑 DSL,进一步降低了自动化的准入门槛。
表面上看我们似乎是照猫画虎造了个 java 版的 RF,还是简易版的,都是用同一种思想解决同样的领域问题。实际上,就像每种编程语言都会有独立的 HttpClient Library,在自动化领域,Library 之于框架的价值等同于 Library 之于编程语言的价值,它们才是背后默默奉献的螺丝钉。虽然 RF 有类似胶水语言的 Library 来解决跨语言的问题,可能并没有 JVM 的内部调用来的可靠。所以,无论是在 Library 和业务贴合性上,还是在框架语言、DSL 选择上,对于 51 现有微服务的架构和技术栈,ATF 都更适合在当前体系下扮演集成测试自动化框架的角色。
4 总结
图 4.1.1 并发执行策略
随着 ATF 支撑的服务数量和 case 数量的增加,我们遇到了很多新的问题。当单服务的 case 数量超过一定数量时,超过了设定的阈值(5 分钟),ATF 新增了用例过滤策略、并发执行策略(图 4.1.1)来缩短执行时间。今年年初我们将 ATF 整合进用例管理平台,通过 web 页面也可以完成自动化用例编写。在实现 remote excutor 时,我们利用 URLClassLoader 自定义管理 Libraries,实现了行为类热加载,目前正在试用阶段,后面还会支持高可用高并发等更多特性。好的框架一定是与时俱进,通过不断迭代解决实际的问题,ATF 还很年轻,我们相信它还会有第四、第五、第 N 次迭代。
引用:
《微服务设计》Sam Newman 著 崔力强 张骏译
BDD https://en.wikipedia.org/wiki/Behavior-driven_development
《从数据驱动到各种驱动》https://zhuanlan.zhihu.com/p/30588403
RobotFrameword wiki https://en.wikipedia.org/wiki/Robot_Framework#cite_note-2
JavaLibCore https://github.com/robotframework/JavalibCore/wiki/Getting-Started
评论 1 条评论