2.2 模块化测试平台的设计方法
在1.3.2节中,我们简单介绍了模块化的概念及其优势,在这一节中,我们继续分析模块化所带来的优势,以及如何才能真正实现自动化测试平台的模块化。
2.2.1 什么是模块化
回顾软件发展的历史,最早的软件架构被称为Monolith架构,所有的产品功能都放在一个巨大的工程项目中,并且功能之间的代码相互纠缠和调用,如图2-1所示,真的“设计得不怎么样”。
图2-1 一个混乱的Monolith架构
在这种模式下,各种功能没有严格的边界,依赖关系混乱,牵一发而动全身,维护成本高昂。随着软件规模的增长,这种Monolith架构对项目的后期维护来说是灾难性的。比如,我们对图2-1中的功能1进行升级,内部逻辑及函数定义的改动,都会对依赖它的功能造成影响,甚至运行不正常。
模块化的架构的出现缓解了这一问题,如图2-2所示。
图2-2 模块化的架构
在定义模块的时候,人们先要确定模块的功能边界,也就是模块具体处理哪一部分功能,并且对功能的调用接口进行了严格的定义和封装,模块内部的逻辑不再直接暴露给外部,所以在图2-2中,调用的箭头被挪到了模块边界上,其他模块只能通过这些接口访问模块的功能。
而在接口设计上,解耦合是一个很重要的概念,所谓耦合就是模块之间相互依赖的程度。比如,在图2-2中,假设功能模块1提供了数据库的查询功能,并通过查询方法返回了数据记录。如果我们提供的查询方法是SQL语句,那么就会产生强耦合,因为该模块可能原本使用MSSQL数据库,后来因为业务需求,改为使用MySQL数据库,而MSSQL和MySQL的SQL语法是有区别的,尽管我们还是通过该模块提供的查询方法来查询,但是原本的SQL语句的参数已经不适用新的数据库的查询,结果是并没有实现模块的替换。因此,我们需要将查询的行为进行封装,让其参数变得和具体SQL语言无关,这样无论该模块换成什么类型的数据库,都能进行正确的查询。
此外,分层架构或者洋葱架构也是被反复提及的概念。在模块功能的设计中,我们需要将具体的业务进行分类,使得模块的调用关系呈现单向性,禁止相互依赖,如图2-3和图2-4所示。
图2-3 分层架构及调用方向
图2-4 洋葱架构及调用方向
不幸的是,在大量的实践中,人们发现,要将设计混乱的Monolith架构平滑地演进到模块化架构,几乎是不可能的事。如果在原本的设计中,功能之间的调用不能做到单向,那么其功能并不能简单地映射成为模块。所以作为自动化测试平台,如果要保证其将来的扩展性和维护性,千万不能抱着先做能用的部分,将来再进行重构的想法,在设计之初就需要考虑模块化带来的便利。
2.2.2 核心功能和业务分离
我们先从一个比较简单的自动化测试流程入手,分析自动化测试平台的功能分类。常见的自动化测试流程如图2-5所示。
图2-5 常见的自动化测试流程
对于常见的自动化测试场景,测试工程师首先要搭建测试环境,包括软件环境和硬件环境,然后建立对测试环境的描述(测试环境配置信息),通过文本或数据库储存环境信息。接着,测试工程师需要设定自动化测试执行的各种参数(测试配置信息),以及测试用例。执行自动化测试的时候,测试用例将测试结果保存到相应的位置,并通过文本或图形的方式显示,最后测试工程师对输出的结果进行分析。
如果我们将这个流程看作与业务无关的流程,也就是说所有的测试团队都能使用,那么这些功能就是这个自动化测试的核心功能。我们要将这些核心功能从整个自动化测试平台中抽离出来,将其和具体的业务进行逻辑分离。很多测试团队并没有很好地实现核心功能和业务逻辑的分离,所以测试平台的扩展性就会受到很大的制约。比如测试环境配置,假设该团队测试的是某种软件产品,测试环境的配置功能会和具体的产品类型强相关,用以描述某个特定的软件产品。或者在测试执行的过程中,包含了特定的代码逻辑来操作特定的软件产品。
在设计之初,核心业务的总结和抽象是一个必要的过程,但是我们并不能保证所有必要的功能都在设计之初就很好地被总结出来,所以核心功能和业务的分离是开发过程中的一个持久化的过程。当在开发过程中发现一个功能可以被大量地复用时,就要考虑这些功能是否能够被抽象出来,并且添加到核心功能中。
2.2.3 分层设计思想
在2.2.1节中我们提到了模块化的分层思想,本节就来讨论一下分层思想。分层思想是软件设计模式中比较流行的一种模式,简单地说就是底层的代码负责具体的技术,而上层的代码负责业务。也就是说,越往上,其技术属性(比如具体的通信协议、数据库类型等)就越弱,但是业务属性就越强,比如具体做了什么。
一般比较流行的是三层架构,如图2-6所示。
图2-6 三层架构
最底层是数据层,负责具体的技术代码,包括数据库、网络通信和操作系统,这些操作与具体的业务逻辑没有直接关系,但是如果设计得当,该层面可以被不同的项目所复用。
中间层也称为业务逻辑层,主要针对具体的业务逻辑进行封装,业务的实现依赖底层的技术代码所提供的功能。比如对于一个产品的管理功能,通过底层数据库提供的增删改查接口,进行逻辑功能的封装。
最上层也称为表示层,具体负责调用中间层的逻辑,对整个系统的功能进行展示,北向可以是终端用户,也可以是应用程序的北向接口,提供给其他应用程序。
有些设计会把中间层拆成两层,一层用于数据持久化,另一层表示具体的业务。比如我们可以将产品管理拆封成数据库访问操作和业务逻辑操作。在这种情况下,原来的三层架构变成了四层架构,但是笔者认为,这两种设计模式其实是一致的。
将分层的思想应用到自动化测试平台上,得到如图2-7所示的测试平台的三层架构设计。
我们将具体的技术代码封装在架构的最底层,比如和测试设备之间的通信协议的实现、UI测试中对UI元素的封装、测试结果的储存、测试资源的序列化等操作,这些操作本身和测试业务逻辑无关。对于中间层和表示层,我们分为两部分——测试平台和测试用例,它们都是基于数据层之上来构建的。测试平台引用底层提供的功能,对特定的业务逻辑进行封装,并提供给表示层。而测试用例可以使用测试平台所注入的资源信息,也可以基于底层提供的功能封装测试过程(这一方法笔者并不推荐,笔者认为通过测试平台去耦合是一种比较好的设计方法),最终将测试逻辑提供给测试用例去调用。
分层设计会让我们在平台实现初期的工作量增加,但是其带来的好处是巨大的。
图2-7 测试平台的三层架构设计
2.2.4 前后端分离
任何一个软件都少不了前端功能,友好的前端不仅能极大地提高测试工程师的配置和执行效率,还能降低学习成本,让新手更容易上手。一般说起前端,我们很容易想到前端图形界面,比如现在很火的网页前端设计。实际上,更广义的前端概念指的是软件展示给使用者的接口,而图形界面只是其中的一种,还包括命令行模式等。
相对于前端,具体软件的业务逻辑处理的部分称为后端。在早期的软件设计中,前后端很容易交织在一起,比如早期的WinForm程序、Swing、网页设计中的ASP等,都是前后端融合的典型(虽然Swing也支持MVC模式,但是笔者不认为它是严格的前后端分离的框架),后端的逻辑处理代码和前端的逻辑代码交织在一起。这种模式随着代码量的增加,升级和维护变得极其困难。假设我们需要更换一个前端的图形控件,它很有可能牵涉后端的逻辑,从而使修改变得特别麻烦。
之后人们尝试着将前端和后端进行分离,出现了类似于MVC、MVVM的概念,前端只处理前端本身的逻辑,比如图形展示、文字的格式化等,后端也只处理后端自己的业务代码,前端和后端通过某种机制来耦合。
我们通过MVC的概念来说明前后端分离的概念。M指的是Model-数据模型,V指的是View-视图,C指的是Controller-控制器。视图可以理解为前端,主要用于对数据的呈现,模型可以理解为后端,主要负责对业务的处理,而控制器主要负责接收用户的输入并协调视图和模型。M、V、C三者之间的关系如图2-8所示。
图2-8 MVC设计
我们用一个简单的例子来说明MVC的设计思想。假设有一个后端的数据表示产品记录,它需要展示到前端,并且可以通过前端来设置后端的产品数据,下面我们用Python代码来做一个模拟的实现:
上述代码中,类ProductView表示前端的功能,使用一些print语句来打印产品的信息,而类ProductInfo代表产品记录。如果不采用前后端分离的方式,那么可以在ProductView中直接调用后端的数据,产生耦合点。然后,通过MVC的方法增加控制器并解耦,具体实现代码如下:
上述代码中,我们通过引入ProductController类分离了视图和模型,使得视图和模型的耦合关系松开,通过控制器决定Vi e w的更新和模型的更新,而不是用视图直接调用模型或者模型去驱动视图。今后如果需要修改视图上的逻辑(比如想换一个视图)就可以轻松地完成。
在设计自动化测试平台时,也需要考虑前端和后端的分离。作为一个测试平台,其使用者不一定仅仅是测试工程师,也有可能是其他的软件,比如自动测试执行的调度工具、持续集成工具等。所以针对不同的对象,前端的模式也会有区别,对测试工程师而言,需要图形化的界面。对于自动执行工具,我们需要北向的API接口或命令行,但对于不同的前端模式,测试平台的功能是相同的。
因此,整个测试平台的前后端的逻辑应该如图2-9所示。
图2-9 平台前后端分离
我们通过某种方法来实现类似平台控制器的功能,比如通过内部的API提供给前端,进行不同类型的前端实现。