高效自动化测试平台:设计与开发实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.1 测试资源

为了设计一个可扩展性强的测试资源管理模块,我们首先要分析,在测试过程中所遇到的测试资源都有什么,然后对其进行分析、归纳,从而实现对测试资源的描述,这样当我们需要扩展新的测试资源时,就可以在测试资源的定义中进行扩展。

3.1.1 测试资源和抽象

我们当然不能凭空去想象测试资源的分类及抽象过程,很多实际的测试案例能够帮助我们进行分析和总结测试资源的分类和抽象过程。接下来,通过一些笔者经历过的测试项目的测试场景,来总结一下测试资源的分类和抽象的思路。

1.一些测试场景

笔者曾经任职于某通信设备公司,通常,测试环境所包含的测试资源如下:

• 被测设备:交换机、路由器、无线AP及网关等设备。

• 测试设备:各种流量仪表,测试用PC。

• 辅助设备:终端连接设备,电源控制器。

这些测试资源通过一定的方式连接起来,形成一个测试拓扑,比如当我们测试一个无线网关产品时,可能会有如图3-1所示的拓扑结构。

图3-1 一个AP测试的拓扑图

这些测试资源有如下共同的特点:

(1)都具有一些特定的属性,比如设备的型号、一些静态属性等。

(2)通过管理口进行管理,设备支持特定的连接方法,通过标准或特殊的协议进行设备的访问和操作,比如Telnet、SSH、Web、OAM或一些私有的协议。

(3)都包括可以和其他设备连接的物理接口来执行业务操作,这是网络设备测试的主要目的,比如以太网接口、RF的WIFI、蓝牙接口,等等。

除了网络设备的软件测试,再来看一些软件测试平台的测试资源,比如某个网站的功能测试如下:

(1)测试设备:个人终端,比如安装了浏览器的PC终端,或者手机和Pad。

(2)被测对象:网站及其各个功能模块。

(3)辅助设备:加压设备、网络流量损伤设备、攻击流量生成设备。

这些测试资源也可以通过一定的逻辑拓扑连接起来,如图3-2所示。

图3-2 网站测试的拓扑图

可以看到,这些资源具有和网络设备测试相似的特性:

(1)特定的共同属性,比如访问不同类型的终端支持的操作系统等静态属性。

(2)通过管理接口或者直接访问来进行管理,比如测试程序运行在本机直接调用Selenium接口,或者通过Seleninum Grid进行分布式的调用。

(3)偶尔需要资源之间的逻辑连接关系,比如攻击设备和网络的连接。

2.一种抽象的思路和方法

可以看到,不管我们的应用场景如何,测试对象是什么,测试用例开发者关心的是测试资源的静态属性及其连接信息。所以,我们可以将所有的测试资源都描述成设备,通过设备端口来描述其连接关系。如图3-3所示,所有的测试资源都可以抽象成设备和端口。

图3-3 测试设备的抽象

设备和端口都是非常抽象的概念,它们并不代表某种具体的设备,需要测试用例开发者自己去定义和维护。测试用例开发者通过增加设备的静态描述信息,来说明这个设备具体是什么类型,以及其特有的一些属性。

而端口主要用来表示所需拓扑连接的关系,端口包含一个“远端端口”的属性,表示该端口与远端的某一个端口的连接关系,如图3-4所示。

图3-4 端口及拓扑的抽象

我们先来举一个交换机的例子:

在测试过程中,我们需要获取如下所示的交换机的一些静态属性:

• 交换机设备名称:在测试环境中的唯一标识号。

• 交换机的类型:交换机的产品类型。

• 描述信息:用来具体描述该交换机在测试环境中的作用。

• 管理接口:通过什么方式来管理交换机。

• OS版本:交换机的默认版本。

交换机的数据端口设计的内容包括如下几个静态属性:

• 端口号:交换机的端口号。

• 端口能力类型:比如是速率为100Mbit/s的电口,还是1000Mbit/s的光口。

除了数据端口,我们还希望将交换机连接在程控电源上,所以还需要定义类型为电源的端口。

对于服务器,在测试过程中,我们可能需要获取的静态属性有:

• 服务器名称:服务器在测试环境中的唯一标识号。

• 管理登录地址:比如SSH或者后台的登录地址。

• 操作系统:服务器的操作系统。

• 服务器类型:服务器的硬件类型。

• 服务访问地址:测试的服务访问地址。

• BIOS管理地址:一些服务器支持的远程BIOS管理地址。

• 服务器序列号:服务器的序列号。

• 各种配置属性、磁盘大小、内存大小,等等。

服务器的端口有各种网卡、USB、电源等,比如对于网卡,我们需要定义如下参数:

• 网口速率:网口的连接速度。

• 网口芯片提供商:该网口芯片的提供商。

我们可以定义ResoureDevice和DevicePort类来描述设备和端口的概念,代码(core/resource/pool.py)如下:

ResourceDevice代表资源设备,封装了一些基本的属性。ports(字典属性)用于保存设备端口对象DevicePort的实例。DevicePort除了具有一些基本的属性,还有一个parent属性,用于保存其父的对象实例,也就是该端口所处的设备,remote_ports(列表属性)用于储存其连接的远端的端口实例。

由于Python中可以动态添加实例,所以我们并不需要预先定义所描述的设备的一些属性,可以在反序列化的过程中通过setattr方法动态添加。

3.1.2 测试资源的序列化和反序列化

使用测试资源类的另一目的是,让测试资源能够通过序列化保存成相应的文本文件,比如JSON、XML或YAML。同样,我们可以通过文本文件来反序列化成测试资源类的实例,如图3-5所示。

图3-5 测试资源的序列化和反序列化

通过测试模块提供的序列化和反序列化可以简化开发者的工作,当要增加一个属性,或者添加一种新的测试资源时,开发者可以不用关心具体的序列化和反序列化的实现,而专注于测试资源的设计。

Python提供了一些库,可以做一些简单的序列化和反序列化的工作,比如内置的JSON包可以将字典或列表序列化成JSON字符串或文件,反之可以将JSON字符串反序列化成字典或列表。对于我们设计的测试资源类,并非一个字典或列表,可以通过一些简单的扩展来使其支持类的序列化和反序列化。

笔者比较喜欢使用JSON的原因是,在Python中JSON的序列化非常简单好用,但是如果读者喜欢使用XML甚至YAML,或者因为项目需求使用其他编程语言,思路是一样的。Python也有一些比较好用的XML和YAML包支持XML和YAML文件的操作,在此不做具体的讲解。

3.1.2.1 资源类实例的序列化

Python内置的JSON包中,可以使用dump方法和load方法来操作字典对象的序列化和反序列化,先将类实例转换成相应的字典描述,再通过JSON的dump方法转换成JSON的字符串输出到文件。

我们如何将一个类的实例转换成字典呢?考虑到Python对象内嵌了一个魔术属性__dict__,我们可以将对象内的所有字段和值转换成字典形式,代码如下:

输出的结果为:

似乎我们可以用这个方法来实现资源类的序列化,但是通过__dict__方法,我们只能获取简单类型的值的序列化,如果某个字段的值是一个类实例,那么我们只能得到这个类实例所对应的内存地址,具体实现代码如下:

输出结果如下:

属性field4并没有正确地被序列化。

另外一种方法是,直接使用JSON包中的dump方法的default参数来定义dump时的默认操作,实现代码如下:

输出结果如下:

似乎成功了!但是这个方法有两个大问题:

第一,无法处理递归,来看下面这段代码:

上述代码会产生错误的输出结果ValueError:Circular reference detected。事实上ResourceDevice和DevicePort就存在相互引用的关系。

第二,在做反序列化的时候无法确定实例的类型,因为当一个类转换成为字典之后,其类的属性并没有被保存,反序列化的时候无法知道需要实例化成什么对象。

所以我们还是需要针对需求设计自己的序列化过程。基本思路还是利用__dict__,但是我们要自己控制其序列化的流程。对DevicePort来说,使用循环遍历所有的属性,如果是parent和remote_ports字段,则做特殊处理,只储存parent的名称,以及远端端口实例的parent和端口名称,以避免递归的产生,其他字段则直接处理。我们为DevicePort类添加方法to_dict,以下代码(core/resource/pool.py)实现了DevicePort的序列化过程:

同样,对于ResourceDevice,我们也使用相同的方法。在处理字段ports的时候,直接调用DevicePort的to_dict方法,就能得到端口实例的序列化结果,实现代码如下:

从上面代码可以看到,我们把资源类的实例转换成了相应的字典类型数据,这会方便资源池的进一步处理,测试代码如下:

上述代码新建了两个ResourceDevice的实例,表示两个交换机,每个交换机都有两个端口,并且交换机1的ETH1/1端口和交换机2的ETH1/1端口连接在一起。我们使用switch.to_dict方法来输出序列化的结果:

3.1.2.2 资源类实例的反序列化

反序列化是序列化的逆向操作,我们需要将字典类型的数据反向转换成为相应的类对象。虽然在序列化的时候丢失了类的信息,但整个类及序列化后的结构是定义好的,也就是说,反序列化后的实例的类型是特定的。

首先,对于端口的反序列化,我们需要输入整个端口的字典信息。然后,通过字典中的字段,生成一个DevicePort的实例,代码如下:

from_dict是一个静态方法,直接返回一个新的DevicePort的实例。DevicePort的parent需要ResourceDevice的实例,这个实例通过参数parent进行传递。然后,除remote_ports和parent外,将字典中所有的key设置成该实例的属性。由于JSON文件是文本文件,使用者可以很方便地使用文本编辑器来编辑、添加或删除一些字段设置,这样通过setattr方法能够将一些原本没有定义在DevicePort类中的属性添加进去。

同样,ResourceDevice的反序列化方法实现代码如下:

当处理到字典中key为ports的时候,我们遍历字典中的ports,通过DevicePort的from_dict新建DevicePort的实例,并添加到ResourceDevice的ports中,所有DevicePort实例的parent均为新建的ResourceDevice实例。

读者也许会发现,我们并没有处理端口的remote_ports。也就是说,这里的反序列化过程并没有完整地复原序列化之前的类的实例中的所有信息。原因是,我们在处理反序列化的时候,并不能控制反序列化的顺序,比如3.1.2.1节中最后的例子,我们在反序列化switch1的时候,switch2的实例并没有反序列化完成,无法获取switch2的实例及其端口的实例。所以,我们需要再做第二轮操作,在所有的实例都反序列化完成之后,完善拓扑的连接信息。我们将在下一节实现这部分功能。

3.1.3 测试资源池

当我们有了测试资源之后,希望能够将这些测试资源进行统一管理,针对不同的测试场景和目的来定义不同的测试环境,于是就有了测试资源池的概念(Resource Pool),有的团队称其为测试床(Test Bed),或测试环境(Test Environment)。测试资源池是一组测试资源的集合,测试平台可以对测试资源池进行统一的管理,我们可以给测试资源池设计一些功能,提供给测试用例开发者进行调用。

3.1.3.1 管理测试资源的序列化和反序列化

3.1.2节介绍了测试资源的序列化和反序列化,但是这些序列化后的测试资源如何储存,如何管理,才能方便日后用户的使用呢?由于测试资源池是一组测试资源的集合,所以我们可以通过测试资源池来管理测试资源的序列化和反序列化。

我们定义ResourcePool类来代表测试资源池,测试资源可以作为这个类的一个列表属性,来储存测试资源类的实例,并通过保存方法生成统一的序列化文件,再将其作为一个测试资源池的描述文件,通过读取方法来读取不同的测试资源文件,生成不同的测试资源池实例。不仅如此,我们还可以添加测试资源池的一些本身的属性,比如测试资源池的一些描述信息。

下面是一个测试资源池对象的实现代码(core/resource/pool.py):

ResourcePool包含了topology属性,用于储存所有的资源实例,information属性用于存放描述资源池本身的信息。

方法save则将topology属性中所有的资源对象的实例序列化成字典,存放于序列化字典的devices字段内,并且将information属性保存在序列化字典的info字段内,最后通过json.dump方法输出到文本文件。

方法load则从JSON文件中读取JSON内容并将其转换为字典类型,然后反序列化devices字段,使information字段的内容成为相应的对象实例。可以看到,我们在3.1.2.2节中没有实现的反序列化功能在这里实现了。在所有的ResourceDevice和DevicePort对象反序列化之后,我们再次遍历devices字段下的所有对象及其端口信息,通过序列化后的remote_ports中定义的device和port信息,找到反序列化后的对象实例,最后保存在相应的remote_ports中。

我们继续通过3.1.2.1中最后的例子来测试这段代码:

在上面这段代码中,我们使用了ResourcePool类的save方法,将整个拓扑结构保存在test.json中,test.json的代码如下:

同样,我们可以在最后的print语句上设置断点,在load方法执行后查看变量rp的状态,如图3-6所示。

图3-6 反序列化后调试器内的对象结构

3.1.3.2 资源占用管理

并不是所有的测试团队都很“富裕”,大多数情况下,测试资源需要共享。当多人同时使用同一套测试环境的时候,就会产生冲突,有可能工程师小李在使用环境A的时候,工程师小张也在环境A上执行测试用例。我们并不希望这样的情况经常发生,所以可能在运行测试用例之前,小张需要通过聊天工具或者发邮件的方式,来询问小李是否在使用环境A,以此来确认不会产生冲突。

如果我们把测试资源池文件通过某种方式共享,比如所有的测试资源池文件都保存在同一个网络文件的存储目录(Samba)中,就可以建立一种锁的机制。当小李通过load方法使用环境A的描述文件时,同时使用reserve方法设置该资源池文件中某个JSON字段值,比如reserved字段,其中包括占用者、占用时间等信息。当小张也通过load方法使用环境A时,发现资源已经被占用,于是,小张就无法使用环境A了。直到小李释放了资源,reserved字段被删除,小张才可以使用环境A,并且生成自己的reserved字段。

我们为ResourcePool类添加reserve和release两个方法,记录用户占用和释放当前资源的相关信息,然后在load方法中添加判断逻辑,以确定当前资源没有被占用。改进后的代码如下:

如上代码所示,ResourcePool类新增了reserved属性用于存放占用的信息,该信息是结构为{“owner”:xxx,“date”:xxxx}的字典信息。属性file_name用来存放当前load的文件名,owner用于存放当前的用户信息。

我们在load方法中添加了file_name属性,用于保存当前的文件名,并检查当前读取的资源配置文件中是否有reserved字段,以及reserved字段中owner的值是否和当前的owner的值一致。

reserve方法执行前需要先读取文件,以保证从前一次读取到执行reserve方法之间没有其他人占用该资源。然后通过设置reserved属性调用save方法,更新文件内容。

release方法同样在执行前执行读取操作,以确保文件是否被当前操作者所占用,然后清空reserved属性并调用save方法,更新文件。

这种占用管理的方法可以结合用户系统一起使用。

这种方法虽然是防君子不防小人的机制(其他用户可以随时修改资源文件,删除reserved字段),但是作为一种通知机制,它可以告诉其他使用者潜在的冲突情况。