谈一谈软件测试中的测试数据准备

测试数据准备方法以及未来的发展方向

本文已被阅读过 Posted by Chunming Liu on 2019-04-16

测试数据的准备,是软件测试工作中非常重要的环节,无论是手工测试还是自动化测试都避不开测试数据准备工作。今天我们就来聊一聊测试工作中常用的测试数据准备的方法,深入了解各自的优缺点和使用场景,以及测试数据准备工作未来的发展方向。

常见的测试数据准备方法

我总结了一下我曾经过用过的生成测试数据的方法,主要有以下几类:

  • 基于GUI的测试数据生成方法
  • 基于API的测试数据生成方法
  • 基于DB的测试数据生成方法
  • 基于MQ的测试数据生成方法
  • 基于第三方库方式的测试数据生成方法
  • 综合运用上述方法生成测试数据

接下来,我们一起详细分析一下各种方法的有权点以及适用场景。

基于GUI准备测试数据

基于GUI界面进行测试数据准备,是最原始的创建测试数据的方法,这种方法其实是采用E2E的方法来执行业务场景,然后得到测试数据。

比如,测试用户登录功能,那么需要准备的测试数据就是用户账号,为此我们可以通过APP或者WEB端的GUI页面注册新用户,然后用这个用户完成用户登录功能的测试。

这种方法的优点是简单直接,创建的数据来自于真实的业务流程,最大程度保证了数据的正确性和完整性。在很多手工测试的场景中,这种方法被普遍采用。

但是,这种方法的缺点也非常明显,主要体现在以下几个方面:

  • 创建测试数据的效率低,不适合批量生成测试数据。 因为通过GUI操作每次只能创造一条数据,而且通过手工操作GUI的过程也是比较耗时。
  • 基于GUI的测试数据生成方法不适合为自动化测试提供数据。 由于自动化测试往往是通过代码来准备测试数据,而GUI方法生成测试数据的方法不太适合封装成代码被自动化测试用例调用。因为封装GUI方式生成测试数据的方法,本质上是在开发GUI自动化测试用例,而我们知道无论是开发工作量还是执行效率,亦或是稳定性方面,这种方法都是不是最佳的选择。
  • 会引入不必要的测试依赖。 比如测试用户登录功能,如果依赖GUI先注册一个用户,那么就意味着注册功能必须是没问题的,引入了依赖。这种情况,从数据库中找到一个已注册的账号来测试登录功能才是最佳选择。

在前后台配合的手工测试中,比如内容管理系统CMS和手机APP的测试,如果要手工测试手机APP的文章列表功能,那么就可以采用这种方法。除此之外,基于GUI操作生成测试数据的场景并不多。

基于GUI生成测试数据的方法,有一个非常重要的价值是帮助我们在创建测试数据的过程中,找到创建数据的过程中都调用了哪些API以及修改了哪些DB的表。只有了解了这两个方面,我们后续通过API或者修改DB方式创建测试数据时,才能保证数据的完整性。

基于API准备测试数据

通过调用API生成测试数据,是目前测试数据生成的主要方法。由于后台接口一般比较稳定,大大提供了测试数据构造的准确性和成功率。调用接口相比GUI操作也能够比较快速的创建测试数据,效率高。另外,由于我们直接给API传递参数,通过参数的组合可以构造成某些GUI方法不能构造出来的测试数据。

那么,我们如何获取到这些API呢?通常推荐按照下面的顺序,来查找API相关的信息。

  1. API接口文档。 通常成熟的开发团队,都会编写API的接口文档,接口文档中会详细描述接口的URI和调用参数,这是最直接有效的办法。
  2. 通过抓包。 抓包在测试中是非常常用的辅助手段,我们可以在操作APP或者WEB页面的时候,对操作进行抓包,通过对抓取到的请求包,分析接口的各种参数。这也是相对高效的办法。
  3. 查看日志文件。 对于已经上线的接口,我们可以通过服务的日志,来查看接口调用过程中的URI和参数等内容。
  4. 阅读源码。 如果前面三种方法都不能用,那么可以在Gitlab上查看开发人员的项目代码,通过阅读代码的方法,找到接口请求的各种参数。

通过API构造测试数据的方法也不是完美的,主要有几个方面:

  1. 不是所有的数据创建都有对应的API。
  2. 有时候需要顺序调用多个API。 有时候测试数据之间是有关联关系的,为了保证测试数据的完整性和一致性,需要依次调用多个API,无形中增加了测试数据准备的复杂性。

调用API创建测试数据,天生适合与自动化测试相结合,在实际的测试实践中,我们往往会把API封装成测试数据准备函数供自动化测试用例使用。当API内部逻辑有修改时,我们依旧可以通过封装函数来准备测试数据,对测试用例来说,是完全透明的。

这里所说的API指的是基于HTTP协议的Restful API。但是可以扩展到其他协议的各种调用接口,比如MQTT协议、RPC协议等。

基于DB准备测试数据

通过往数据库中直接插入数据,也是非常常用的构造测试数据的方法。具体做法是,将创建测试数据的SQL语句封装成一个个测试数据生成函数,当我们创建数据时,直接调用这些封装好的函数即可。这种方法有一个非常大的优点是生成测试数据的效率非常高,可以短时间内往数据库中插入大量的测试数据。

以用户登录功能测试为例,当我们调用API进行用户注册时,这个API会将用户的详细的信息插入到user表和role表两个数据库表中。如果我们采用数据库方式创造数据时,给user表和role表分别插入对应的数据就完成了用户的注册。我们还可以直接使用DB中已有的数据作为我们的测试数据,从而省去了很多操作。

这里的前提是,你必须知道进行新用户注册时,到底涉及到了哪些数据库的表。最直接的办法就是跟开发同学索要SQL语句,或者查看源代码。

这种构造测试数据的方法也不是完美的,主要体现在以下几个方面:

  1. 有的测试数据准备涉及到的数据表太多。 导致封装和维护测试准备函数的成本比较高。
  2. 容易出现数据不完整和不一致。 比如服务A某一个业务,实际会在服务A的数据库表A和数据库表B中分别插入数据,并且同时会给kafka的某个topic发送数据供服务B消费处理后持久化到服务B的数据库表C中。如果我们漏掉了某个数据库表的插入操作,可能会导致数据的不完整和不一致。

基于DB准备测试数据的方法,通常作为API方法的补充。

基于MQ准备测试数据

在微服务架构中,通常会存在通过消息中间件将多个服务进行解耦,为了减少测试工作的依赖,通常会往kafka中构造测试数据。

比如,两个服务是通过KAFKA进行消息传递的,两个服务分别作为kafka的生成者和消费者。当我们测试作为消费者的服务时,就可以编写kafka的producer代码,往kafka中生产测试所需要的测试数据。具体的做法与通过DB构造测试数据的方式类似,将kafka的producer代码封装成测试数据生成函数,当我们创建数据时,直接调用这些封装好的函数即可。

这种做法和操作DB并没有本质不同,其优点和缺点也是类似的。

基于第三方库准备测试数据

我们的测试实践中,经常会需要生成很多随机的数据,对于这类需求,直接使用代码封装成函数生成数据。拿python为例,可以自己结合random()之类的函数随机生成数据,还可以使用faker(项目地址)这样的第三方库来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from faker import Factory

fake = Factory().create('zh_CN')

def random_phone_number():
'''随机手机号'''
return fake.phone_number()

def random_name():
"""随机姓名"""
return fake.name()

def random_address():
"""随机地址"""
return fake.address()

def random_email():
"""随机email"""
return fake.email()

这类生成测试数据的方法试用的场景是,对数据本身的值不关心但是测试中又必须需要这些参数的情况。

综合运用上述方法准备测试数据

在实际工作中,很少使用单一的方法就能满足测试的需求,往往是综合运用上述各种方法、一个典型的应用场景是,通过API生成最基础的测试数据,比如车辆的vid,然后使用数据库和MQ的方法是生成符合测试需要的车辆状态数据。

我以上报车辆状态数据的测试为例子,来分享一下具体的如何将API调用和MQTT协议的方法结合起来构造测试数据。

比如我们要测试云端对车辆上报的报警数据的处理是否符合要求。首先,我们需要通过调用注册Vechile ID的接口来注册一台车并且获得车辆证书,通过调用这个接口我们可以得到一个车辆的ID以及证书数据。再结合MQTT协议产生车辆的报警数据。

为了构造测试数据的更加便捷,我们往往是对上面的操作进行封装。用封装后的方法产生测试数据。

准备测试数据的时机

前面介绍了准备测试数的方法,那么应该在什么时候创建好所需要的测试数据。是在测试用例执行中创建测试数据(On-the-Fly方法)还是在测试执行前就准备好测试数据(Out-of-Box方法)。

其实,创建测试数据的时机要根据实际的需要来。主要参考一下几个因素:

  1. 创建测试数据所需要的时间。 如果创建数据需要花很长时间,那么最好采用Out-of-Box方法,在测试执行之前就准备好,以减少整个测试执行的时间。
  2. 测试数据是否需要经常变动。 如果测试数据不需要经常变动,那么最好采用Out-of-Box方法。如果事先生成数据在测试用例中会失效,比如具有有效期的数据,那么就适合采用On-the-Fly方法,在测试执行中创建。
  3. 测试数据是否存在于很多系统。 如果测试数据需要在很多系统中都要创建各自的部分,各自又有很多依赖关系,那么就适合Out-of-Box方法。因为在测试用例执行中创建,会导致测试代码比较臃肿,不够清晰。
  4. 构造测试数据的服务是否稳定。 在不太稳定的服务中构造测试数据,会产生大量构造测试数据失败的情况。这种情况下采用Out-of-Box方法还是比较明智的。

接下来,我们详细看一下On-the-Fly方法和Out-of-Box方法各自的特点,以及适用场景。

实时创建(On-the-Fly)

实时生成测试数据的方法,指的是在测试用例代码执行过程中即时创建测试数据。比如,测试车辆驾驶中,不能执行远程控制命令的场景。在测试执行中,可以通过封装的MQ方法设置测试车辆的车辆状态处于驾驶中,接下来就可以测试远程执行命令了。

On-the-Fly方法创造的测试数据通常是对每一个测试用例起作用的,不同的测试用例都有自己专属的测试数据。像这种车辆状态数据就适合采用On-the-Fly方法创造,这种状态数据通常是每个测试用例都不同。这种构造测试数据的好处是,避免测试数据在测试用例执行前被修改而产生非预期的测试结果。这样的测试数据使用完之后,通常在测试用例结束之后,恢复成原始数据,避免影响其他测试用例。

在自动化测试发展早期,测试实践中通常都会这种方法,也是比较好的方法。他解决了测试用例之间数据之间干扰的问题,也避免了测试完之后的脏数据问题。但是随着软件架构的发展,以及测试频率的提高,这种方式的弊端也逐渐显示出来了,主要有以下几个方面:
首先,有的测试数据比较耗时。 在测试用例执行过程中实时创建测试数据,会导致测试用例执行的时间被拉长。如果测试用例特别多,测试频率又特别高,那么测试时间就变得特别长,这显然不适合现在互联网软件的迭代节奏。为了解决测试耗时的问题,可以采用Out-of-Box方法。
其次,测试数据本身之间复杂的关联性导致构造困难。 很多时候,你为了测试某一个场景,需要构造一堆相关联的测试数据,也是偏向业务链后台的测试数据,这个问题越明显。

比如,要测试被授权人对车执行远程控制命令的场景。会需要车主账号、被授权人账号、车辆ID、车辆ID与车主账号绑定,车主给被授权人授权车辆等前置数据。如果在测试用例执行中准备这些测试数据,那肯定是崩溃的。如果每一个测试用例都这么做,一定会导致测试时间变得非常长。为了解决这个问题,可以考虑将一部分稳定的数据事先创建好,比如车主账号、被授权人账号、车辆ID以及授权关系等数据。

微服务架构的流行导致成功生成测试数据的稳定性降低 现在大量互联网应用采用微服务架构,不同功能划分为更多的微服务独立开发和部署,很多时候测试环境里面,这些微服务并不是100%可用的。也就是说,不是任何时候构造测试数据都能成功。比如你测试的微服务B,需要依赖微服务A构造数据,而这时候正好微服务A不可用,这就block了微服务B的测试。

为了解决上面的问题,事先准备测试数据的Out-of-Box方法,就有了用武之地。

提前准备(Out-of-Box)

Out-of-Box方法,指的是在测试用例执行前,就已经准备好了所用的全部或者部分测试数据,而不是在测试用例中实施创建。因此,执行测试用例时候,可以节省不少准备测试数据的时间,同时也避免因为依赖的测试数据准备服务不可用导致测试被block的情况。

那么Out-of-Box方法是否也存在缺点呢?

最主要的问题是**“有效性”**问题,就是有测试执行中发现测试数据不可用的风险。比如,测试被授权人远程执行车控命令的场景,当你执行测试时,发现被授人的身份已经被车主账号删掉了,这样就导致测试用例执行失败,也就不能顺利完成测试了。

由此可见,这些实现创建好的测试数据,有可能在测试用例执行时已经不可用了,因为这些数据有可能已经进行了非预期的修改。比如,在其他测试用例执行时,使用了这个测试数据,并修改了这些数据的状态。

为了解决这个问题,我们通常采用优化测试管理流程,让不同的测试人员、测试业务都有自己独立的测试数据,并且统计在confluence、jira或者其他公共平台上,大家严格遵守,不要乱用测试数据。

另外,Out-of-Box方法不适合准备,只能被使用一次的测试数据,只会使用一次的测试数据还是采用On-the-Fly方法准备比较合适。

实际工作中,我们通常是采用On-the-fly 和 Out-of-box 这两种方式相结合的方式来准备测试数据。我们可以根据测试目的的不同,将测试数据划分为“固定数据”和“易变数据”。比如某些测试场景中,车辆ID、车辆Profile、车主账号等信息是相对稳定、不经常变化的数据,那么我们可以将这些测试数据称为“固定数据”,这类数据适合采用Out-of-box方式创建。但是在某些测试场景中,比如车辆ID的注销,车辆Profile的变更测试,那么车辆ID、车辆Profile就不能叫做“固定数据”而是应该叫做“灵活数据”,这类数据适合采用 On-the-fly 方式准备。

综合运用这两类方法,可以满足大部分测试数据准备的场景。可以解决准备测试数据耗时长、准备测试数据成功率不高等问题。

构造测试数据的痛点及应对

前面,我们分析了两种准备测试数据的时机以及各自的优缺点。那么我们实际工作中,准备测试数据的工作有哪些痛点,我们又该如何解决呢?

调用封装函数的复杂性

前面提到封装API到一个函数,然后调用这个函数来构造测试数据的方法。但是这种封装方式会有问题,就是如果参数非常多,那么你调用它来构造数据时,就要准备这些参数。如果这些参数是基本类型的话还好,如果参数本身也是对象的话,可能就会更加麻烦了,因为你要创建这些对象。而创建这些对象,有可能要继续调用其他封装的函数,从而牵连出一系列函数调用的操作。

比如,调用这样一个封装了注册车辆vid的函数:

1
2
3
4
5
6
7
8
9
10
def  register_vehicle(vin, color, plate_number, model, misc):
# 封装的处理过程
retrun vid

vin="chunming12345678997"
color="blue"
plate_number="D12345"
model="PAD8"
misc="demo"
vid = register_vehicle(vin, color, plate_number, model, misc)

由此可见,每次使用封装的函数准备测试数据时,我们要给函数传递所有的参数。其实大多数测试场景下,所有参数都可以给一个默认值,用这个函数准备测试数据时,只需要给那些有明确要求的参数传值,其他参数保持默认值即可。这样封装的函数就变成这样:

1
2
3
4
5
6
def  register_vehicle(vin="chunming", color="blue", plate_number="D12345", model, misc="demo"):
# 封装的处理过程
retrun vid

vin="chunming12345678997"
vid = register_vehicle(vin=vin)

这样,大大减少了调用封装函数的成本。当测试用例中只需要一个特定vin的车辆时,只需要给register_vehicle传递参数vin的值,其他的测试用例不关心的参数都可以保持默认值。

封装函数的版本管理

通常我们封装函数是给所有的测试项目共同使用的,这样才能最大化封装函数的价值。共享封装函数的办法通常是将其打包,然后在其他项目中引用。如果你的测试项目是使用Python,那么可以将封装的函数用setuptools打包上传到公司的Pypi平台,在测试的项目中用pip安装。如果你的测试是使用Java,可用Maven将封装函数打成Jar包并上传到公司的私有仓库,在测试项目中的pom.xml中引入jar包就好了。

现在的互联网应用版本迭代更新特别快,导致封装函数也要对应的迭代更新。这就会产生数据准备函数的包升级更新比较频繁,包的版本号就要随着变化。所以引用了这些数据准备函数包的项目,就要更新包的版本。给使用者带来了一些麻烦。

为了解决项目对封装函数的依赖问题,我们可以将其做成Restful API,这样使用者就免去了频繁更新这些依赖包的麻烦。而且Restful API天生的跨平台支持,让调用方不管是用Java写测试用例还是Python写测试用例,都可以得到完美的支持。接下来我们就详细介绍一下基于Restful API的测试数据准备方案。

统一测试数据生成平台

前面介绍了创建测试数据的主要方法、创建测试数据的时机,以及测试数据生成中的痛点。随着测试技术的发展,测试数据准备技术与架构也需要逐步进化,来满足互联网微服务架构的发展趋势以及快速迭代的特点。

现在业界,将测试数据准备的工作进行平台化,逐渐成为测试数据准备方案的发展方向。而Restful API的测试数据准备方案,正好适合平台化的发展方向。我们可以将基于 Java 开发的数据准备函数用 Spring Boot包装成Restful API,或者将基于Python开发的数据准备函数用Flask或者Django REST framework包装成Restful API。

这样一来,测试人员可以通过Restful API调用来准备测试数据了,由于HTTP协议是跨平台的,所以几乎所有的测试框架都可以直接使用这些Restful API准备测试数据。由于使用Restful API提供测试数据,这样方便我们将提供各类测试数据的服务整合到一起,形成“统一测试数据生成平台”。结合Swagger提供的界面化文档,可以方便看到接口调用的方法,并且可以直接在界面上调用接口生成数据。既满足自动化测试的需要,也能满足手工测试的需求。

目前为止,我们将测试数据准备工作进行了服务化,下图就是一个统一测试数据生成平台的Restful API 界面:

统一测试数据平台Restful API UI 界面

"统一测试数据平台"从提供的测试数据特性来分,可以分为真实数据和Mock数据。真实数据就是封装微服务的接口,在业务系统中实际产生真实的业务数据用作测试数据。Mock数据是指通过mock技术产生非实际业务中的数据、这类数据一般用于解决服务依赖问题。

提供真实数据

下面通过Flask Web框架来介绍如何通过封装业务操作提供真实的测试数据实践。

比如,我要测试远程控制车辆的API,其中有一个测试用例是验证在车辆在行驶中时不能进行远程控制。针对这个测试用例,我们需要的测试数据有被控制车辆的ID以及车辆状态。下面以准备车辆ID的Restful API为例,介绍具体的实现方式。

首先,使用pipenv创建虚拟环境,安装好Flask框架。

1
2
3
4
mkdir flasky
cd flasky
pipenv --python 3.6
pipenv install flask

下面这段代码是封装了业务接口api/1/in/vehicle/profile的代码片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import time
import uuid
import requests
from flasgger import swag_from
from flask import Flask, jsonify,request

app = Flask(__name__)

@app.route('/api/1/vid', methods=['POST'])
@swag_from('./register_vehicle.yml')
def register_vehicle():
args = request.args.to_dict()
if 'vin' not in args:
return jsonify(result_code="failed",
request_id=str(uuid.uuid1()),
server_time=int(time.time()),
debug_msg="vin is required",
data={})
payload = {
"vin": args.get("vin", "chunming12345678997"),
"color": args.get("color", "blue"),
"plate_number": args.get("plate_number", "京D12345"),
"model": args.get("model", "ES8"),
"misc": args.get("misc", "demo")
}

r = requests.request("POST", "https://example.com/api/1/in/vehicle/profile", data=payload)
response = jsonify(result_code="success",
request_id=str(uuid.uuid1()),
server_time=int(time.time()),
data=r.json())
return response

if __name__ == '__main__':
app.run(debug=True)

在这段代码中,提供默认参数,只需要传递测试感兴趣的参数就可以,不感兴趣的数据保持默认值即可。结合flasgger提供的swag_from装饰器,给接口编写文档,让封装的接口易懂和易用。

提供Mock数据

什么情况下需要Mock数据,比如:服务A调用服务B的Restful API,传递给服务B数据,服务B会根据数据情况返回给服务A一个ACK值。当服务B没有Ready时候,我们就需要模拟服务B的行为。

其实产生这种Mock数据,与前面介绍的产生实际业务数据,方法上并没有不同。只是在Restful API的response构造上,前者构造产生的数据来自与真实的业务接口,而Mock的数据是根据测试需求伪造的。模拟前面提到的服务B的行为,以Flask方案为例,就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import jsonify, Flask
from flasgger import swag_from
import time
import uuid
app = Flask(__name__)

ack = {
"data": {
"ack": 1
},
"request_id":str(uuid.uuid1()),
"server_time":int(time.time()),
"result_code":"success"
}

@app.route("/ack", methods=['GET'])
@swag_from('./register_vehicle.yml')
def get_ack():
return jsonify(ack)

通过控制上面ack的内容,就可以模拟服务B的各种Response了。如果将这个ack的内容存入数据库中,让get_ack函数从数据库中取得ack并返回。再写一个接口用于往数据库中写入ack,那么就可以在自动化测试中随意控制ack的返回内容了。

总结

本篇文章我们梳理了测试数据准备的各种方法,并分析了各自的优缺点及适用场景。测试数据准备的时机上看,对于不常改变的数据适合采用提前准备的方法,对于经常变化的数据在测试用例中准备更好。对测试工作中数据准备的痛点进行了剖析并给出了应对方案。最后,给出了解决测试数据生成痛点的终极解决方案——“统一测试数据生成平台”。

后面,我将专门写一篇博文,详细介绍如何从0搭建统一测试数据平台。