JB的测试之旅-测试数据的准备/构造

栏目: 数据库 · 发布时间: 6年前

内容简介:之前看过相关的测试数据准备的文章,坦白说,看完之后,能记住只有2个:而最近,同学恰好也问到这问题:当时回复就如上面的答复,现在回头想想,的确没想到好的方案,在脑海里,有一个所谓的"终极方案",就是

之前看过相关的测试数据准备的文章,坦白说,看完之后,能记住只有2个:

  • api准备数据;
  • 数据库插入;

而最近,同学恰好也问到这问题:

JB的测试之旅-测试数据的准备/构造

当时回复就如上面的答复,现在回头想想,的确没想到好的方案,在脑海里,有一个所谓的"终极方案",就是 读取接口文档,自动生成测试数据 ,理论上可行,但一直没去做,懒;

JB的测试之旅-测试数据的准备/构造

有啥办法

做过单元/接口测试的同学都知道,其中有一个环节就是 测试数据准备 ,而这一步是不可或缺的一步,也是需要花费大量时间投入的一步;

测试接口前就必须准备好该接口需要处理的数据,而数据又有可能依赖其他的数据,这就提高了准备数据的复杂度与难度;

那到底有什么办法?

  • 基于 GUI操作 生成测试数据;
  • 基于 API调用 生成测试数据;
  • 基于 数据库操作 生成数据;
  • 基于 第三方库 自建数据;
  • 结合 多种方式 生成数据;
  • 导入线上/测试数据;

GUI操作生成数据

基于GUI操作生成数据,是指使用自动化脚本或者人工执行业务流程生成数据。

现在需要测试登录功能,这就需要准备一个已经注册的用户,
此时,可以通过GUI操作来创建一个用户(无论是手工还是自动化脚本),
然后再用新建的用户测试登录;
复制代码

这种方式简单直接,并且数据来源于真实的业务流程,一定程度保证了数据的准确性。

然而,缺点也很明显:

  • 创建数据的效率低 :每次的GUI操作只生成一条数据,并且操作非常耗时;
  • 易封装 :通过GUI操作生成数据的过程,其实就是在开发自动化case的过程,加大了工作量;
  • 成功率不高 :GUI的变化直接会导致数据生成失败;
  • 引入了其他依赖 :数据生成成功的前提,依赖于业务流程的正确性。

一般情况下,基本不会使用这种方式生成数据,除非没有其他更好的方式来创建可靠的数据。

不过, 操作GUI生成数据 是其他两种方式 API调用操作数据库 的基础,因为可以知道一条测试数据创建的过程;

API调用生成数据

实际上使用GUI操作生成数据, 本质上就是在调用API

使用GUI界面注册用户时,实际上调用了createUser的API。
复制代码

要注意的是, 一次GUI操作可能调用了多个API ,一般情况下, 都把调用API生成数据的过程封装成数据准备函数

也许会有疑问,到底要怎样才知道调用了哪些api?

  • 直接问开发;
  • 看源码;
  • 模拟一遍,抓包;

这种方式优势在于:

  • 保证数据准确性;
  • 执行效率高;
  • 封装成函数更灵活可控;

这种方式也不是十全十美,缺点在于:

  • 并不是所有数据创建都有对应的API;
  • 业务很复杂的情况下,需要调用多个API,增加复杂性;
  • 需要海量数据时,即使使用了并发,效率也尽如人意;
  • API依赖性;

因此,业界往往还会通过数据库的CRUD操作生成测试数据;

数据库操作生成数据

数据库生成数据一般做法是, 将创建数据需要的 SQL 封装成函数,然后再进行调用

这样就能直接通过数据库操作,将测试数据插入系统数据库。

还是用户登录,直接往userTable和userRoleTable两张表插入数据,即可完成注册。
复制代码

这样做的前提是,需要知道修改了哪些数据库业务表;

这种方式的优势在于:

  • 效率高,能在短时间内生成批量数据;

缺陷也很明显:

  • 维护成本高,当涉及到很多张表的时候,封装的数据准备函数就需要大量时间来维护;
  • 数据容易缺失,一个业务操作设计到的表往往不止一张,容易遗漏;
  • 健壮性差,SQL语句变化时,封装的函数必须实时同步更新,维护成本很高;

第三方库生成数据

这种方式就比较直接,直接使用代码封装成函数生成数据。

python 为例,可以自己结合random()之类的函数随机生成数据,还可以使用faker这样的第三方库:

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调用或者第三方库生成基础的测试数据,然后使用数据库的CRUD操作生成符合特殊需求的数据。

比如:

# 注册新用户并进行绑卡
1. 使用封装的faker库随机生成姓名,手机号,邮箱等信息,并调用createUser API进行注册;
2. 查询userTableb表获得用户名,然后调用bindCard API实现绑卡。
其中,bindCard API中使用的userID即为上一步createUser API中产生的用户ID;
3. 如有需要,通过数据库操作更新其他信息。
复制代码

以上就是一个常用的创建测试数据的过程;

当然也可以在测试用例执行前通api创建数据,执行后清除数据的方式;

导入线上/测试数据

这个就是直导入线上/测试数据,优点是更加贴近用户,出现问题,可直接模拟,但一般都不提供这种方式,就不细说了;

数据创建时机

准备测试数据的时候,都有什么痛点?

  • 耗时长,导致用例执行时间长;
  • 执行测试时可能会出现原先数据被修改而无法复用的情况;
  • 环境不稳定导致数据异常;

正因上面的原因,数据准备不能随时进行,因为,创建时机很重要;

实时创建

指测试用例时实时创建需要的测试数据,所有数据都必须在测试用例开始前实时准备,比如api方式;

优点:

  • 不依赖测试用例外的数据;
  • 保证数据的准确性和可控性;

缺点:

  • 耗时长;
  • 维护成本高;
  • 数据存在复杂关联性;
  • 依赖性;

提前创建

指在准备测试环境时就预先将需要的数据提前准备好,比如数据库插入;

优点:

  • 节省用例执行时间;
  • 不会因为环境问题导致数据无法创建;

缺点:

  • 脏数据;

所谓的脏数据,是指数据在被实际使用前,已经被进行了非预期的修改;

而脏数据可能的来源是:

  • 被其他使用,并修改了状态;
  • 手工测试时不小心修改了数据;
  • 调试过程修改了数据;

如何解决:

  • 维护一份数据,执行后复原;
  • 数据分类,不同数据区段来分配使用对象,比如0-100是A团队,100-200是B团队,通过流程保证;

该方式不适用于只能一次性使用的场景;

如何抉择

稳定不常变化的数据,或是公用数据,建议使用提前创建的方式(数据库),一般来说,适用于接口测试环节;

只能一次性使用,或经常变化的数据,又因环境不一致,建议使用实时创建的方式(API);

一般来说,接口测试就是用实时创建的方式,用例执行前构造数据,执行后清除数据,这样就能尽可能保证用例之间相互不影响,也避免脏数据的产生;
复制代码

适用场景

一般来说,接口测试,都用

数据准备的方法

大多数采用的方法

大多数企业采用的方法就是,将测试数据准备的操作封装成函数;

举个例子:

def post(self,url,data,code,msg):
         resp= requests.post(readconfig('url', 'url')+url, data=data)
         self.assertEqual(200, resp.status_code)
         self.assertEqual(code, resp.json()['code'])
         self.assertIn(msg, json.dumps(resp.json()['msg']).decode("unicode-escape"))
         return resp
复制代码

这样就可以把数据创建 相关操作 封装成函数,业务方只需要直接调用函数即可;

但, 致命的问题是,参数非常多,也非常复杂 ;如上面的例子,就需要4个参数,而实际工作,可能会多达十几个;

而绝大部分情况下,只需要个别参数,其他参数可以使用默认值即可;

那样,代码就会演变成这样:

def xx(A='',B=True,C="xx"):
    ...
    return jbtest(A,B,C)

def xxx():
    ...
    return jbtest(A,B,C)

def xxxx(A=''):
    ...
    return jbtest(A,B,C)
复制代码

这样封装,对于一些常用的数据组合,可以通过一次调用就生成需要的数据;

对于不常用的数据,可以直接调默认的函数来创建,这样就可以更灵活处理;

但是,也有弊端:

  • 参数越多,封装的函数数量随之增加,最终可能演变成上百个函数;
  • 可维护性差,底层函数会影响所有封装的函数,动一发而牵全身;

大公司怎么玩

既然上面的方法有问题,那能否优化下?同时,大公司怎么玩?

想想老东家,谈不上是小公司,但基本也是用上面的方式,前段时间看了茹炳晟老师也有提及到这点,就是引入 Builder Pattern 封装方式;

Builder Pattern

基本概念

到底什么是 Builder Pattern ,翻译过来是建造者模式,目的就是 将一个对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

没看懂?直接来例子:

不引入Builder Pattern,买车的条件有产地,座位数,油耗:
Car.buy(Country="",Seats="",FuelConsumption="")
随着条件越多,传参随之增加;

引入Builder Pattern:
例子1:买一辆车,没其他要求:
Car.buy();

例子2:买一辆车,中国产的:
Car.withBuildCountry("China").buy();

例子3:买一辆车,中国产的,7座的:
Car.withBuildCountry("China").withSeats("Seven").buy();
复制代码

明白了吗?核心就是 在用户不知道对象的建造过程和细节的情况下,可以直接创建对象

这3个例子,可以反向说明解决了什么问题:

  • 方便用户创建对象时,不需要知道实现过程,只需要给出指定对象的类型和内容即可;
  • 代码复用性 & 封装性,将构建过程和细节进行封装;
1. 工厂(建造者模式):负责制造汽车(组装过程和细节在工厂内) 
2. 汽车购买者(用户):你只需要说出你需要的型号(对象的类型和内容),然后直接购买就可以使用了 
(不需要知道汽车是怎么组装的(车轮、车门、发动机、方向盘等等))
复制代码

结构图

JB的测试之旅-测试数据的准备/构造

组成

建造者模式包含如下角色: Builder:抽象建造者 ConcreteBuilder:具体建造者 Director:指挥者 Product:产品角色

职责

角色 职责
Builder 创建一个Product对象的各个部件指定抽象接口
ConcreteBuilder 实现Builder的接口以构造和装配该产品的各个部件,定义并明确它所创建的表示,提供一个检索产品的接口;
Director 构造一个使用Builder接口的对象;
Product 表示被构造的对象,包含定义组成部件的类;

换种说法

  • 指挥者(Director)直接和客户(Client)进行需求沟通;
  • 沟通后指挥者将客户创建产品的需求划分为各个部件的建造请求(Builder);
  • 将各个部件的建造请求委派到具体的建造者(ConcreteBuilder);
  • 各个具体建造者负责进行产品部件的构建;
  • 最终构建成具体产品(Product)。

优点

  • 将一个对象分解为各个组件,相对独立,不受影响;
  • 将对象组件的构造封装起来,客户端不需要知道内部细节;
  • 可以控制整个对象的生成过程;

缺点

  • 对不同类型的对象需要实现不同的具体构造器的类,这可能大大增加类的数量;
  • 使用范围受限制,只适用于产品组成功能相似的产品,即可复用;

什么时候适用建造者模式

  • 生成的产品对象有复杂的内部结构;
  • 生成的产品对象的属性相互依赖,建造者模式可以强迫生成顺序;
  • 在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到;

例子1-微信公众号消息推送

相信大家在使用微信时,也都收到过消息推送吧,来看看官网提供的一个实例:

{
    "touser":"OPENID",
    "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
    "url":"http://weixin.qq.com/download",
    "miniprogram":{
        "appid":"xiaochengxuappid12345",
        "pagepath":"index?foo=bar"
    },
    "data":{
        "first":{
            "value":"恭喜你购买成功!",
            "color":"#173177"
        },
        "keynote1":{
            "value":"巧克力",
            "color":"#173177"
        },
        "keynote2":{
            "value":"39.8元",
            "color":"#173177"
        },
        "keynote3":{
            "value":"2014年9月22日",
            "color":"#173177"
        },
        "remark":{
            "value":"欢迎再次购买!",
            "color":"#173177"
        }
    }
}

复制代码

具体参数请自行到公众号开发平台查询,这里思考的是,怎么设计通用模板?

方法很多,但是这里给出建造者模式的做法,创建builder类(为了方便,去掉了miniprogram参数)::

# -*- coding: utf-8 -*-
 
from collections import OrderedDict
import json
 
 
# 模版中“data”节点的各个元素的数据结构
class Metadata:
    def __init__(self, value, color):
        self.value = value
        self.color = color
 
 
# 微信消息的建造器
class MessageBuilder:
    __contentDict = OrderedDict()  # 定义整个模版的数据结构,保持添加的顺序
    __dataDict = OrderedDict()  # 定义data节点的数据结构,保持添加的顺序
    __dataNoteNext = 1  # data节点要添加的下一个元素的序号
 
    def __init__(self, touser, template_id, url):
        self.__contentDict['touser'] = touser
        self.__contentDict['template_id'] = template_id
        self.__contentDict['url'] = url
        self.__contentDict['data'] = self.__dataDict
 
    def add_first_data(self, value, color):
        data = Metadata(value, color)
        self.__dataDict['first'] = data
        return self
 
    def add_remark_data(self, value, color):
        data = Metadata(value, color)
        self.__dataDict['remark'] = data
        return self
 
    def add_note_data(self, value, color):
        data = Metadata(value, color)
        self.__dataDict['keynote' + str(self.__dataNoteNext)] = data
        self.__dataNoteNext += 1
        return self
 
    def build(self):
        # 为打印出来看的方便,这里将json序列化后的结果缩进2个空格,并且不把中文转为unicode
        return json.dumps(self.__contentDict, default=lambda o: o.__dict__, indent=2, ensure_ascii=False)
复制代码

有两点要说明下:

  • 建造者内部的字典采用OrderedDict,是为了保持顺序与微信示例一致;
  • 建造者每个方法都返回了本对象的引用;

建造者有了,就来生成消息吧,想起上几天fc的通知:

JB的测试之旅-测试数据的准备/构造

模拟作如上两条微信消息:

if __name__ == '__main__':
    pickup_builder = MessageBuilder('jb', 'template_id_pickup', '') \
        .add_first_data('您有一个快递在蜂巢柜里等你来取哦!', '#173177') \
        .add_note_data('123456', '#173177') \
        .add_note_data('jb快递', '#173177') \
        .add_note_data('789456123', '#173177') \
        .add_note_data('15914255XXX', '#173177') \
        .add_note_data('广州', '#173177') \
        .add_remark_data('元宵节快到了,人不在家,也要把爱寄回家~', '#173177')
    print('生成取件通知微信消息')
    print(order_builder.build())
 
 
    print()
 
 
    takeout_builder = MessageBuilder('user222222', 'template_id_takeout', '') \
        .add_first_data('您的包裹已被取出啦', '#173177') \
        .add_note_data('jb快递', '#173177') \
        .add_note_data('78954', '#173177') \
        .add_note_data('15914255XXX', '#173177') \
        .add_note_data('广州', '#173177') \
        .add_remark_data('点击详情查看物流进度', '#173177')
    print('生成取出微信消息')
    print(send_builder.build())

复制代码

这样看下来,是不是代码清晰多了,而且可复用,好像很不错的感觉~

例子2-组建身体

该例子来源于此处:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import abc

class Builder(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def create_header(self):
        pass

    @abc.abstractmethod
    def create_body(self):
        pass

    @abc.abstractmethod
    def create_hand(self):
        pass

    @abc.abstractmethod
    def create_foot(self):
        pass

class Thin(Builder):

    def create_header(self):
        print '瘦子的头'

    def create_body(self):
        print '瘦子的身体'

    def create_hand(self):
        print '瘦子的手'

    def create_foot(self):
        print '瘦子的脚'

class Fat(Builder):

    def create_header(self):
        print '胖子的头'

    def create_body(self):
        print '胖子的身体'

    def create_hand(self):
        print '胖子的手'

    def create_foot(self):
        print '胖子的脚'

class Director(object):

    def __init__(self, person):
        self.person = person

    def create_preson(self):
        self.person.create_header()
        self.person.create_body()
        self.person.create_hand()
        self.person.create_foot()


if __name__=="__main__":
    thin = Thin()
    fat = Fat()
    director_thin = Director(thin)
    director_fat = Director(fat)
    director_thin.create_preson()
    director_fat.create_preson()
复制代码

上面类的设计如下图,

JB的测试之旅-测试数据的准备/构造

指挥者Director 调用建造者Builder的对象,具体的建造过程是在Builder的子类中实现的;

回到正文,理解完 建造者模式 ,突然发现,好像跟上面的封装概念相似的?

def xx(A='',B=True,C="xx"):
    ...
    return jbtest(A,B,C)
def xxx():
    ...
    return jbtest(A,B,C)
def xxxx(A=''):
    ...
    return jbtest(A,B,C)
复制代码

是的, Builder Pattern 也是封装方式,一般来说,会基于原有的封装再二次封装,这样的好处就是业务方无需关心内部逻辑,营造用的好爽的感觉,而 Builder Pattern 内部还是使用api或者数据库的方式来创造数据,只是进行 易用性封装 而已;

JB的测试之旅-测试数据的准备/构造

对业务方来说,是用的爽,对于维护者来说,苦的一逼,详情请看上面的缺点,简单就是 维护成本高 ,容易出现 动一发而牵全身 ,一般来说,只有大厂才会做这事;

平台化

建造者模式是一种设计的思路,因此可适用于不同语言,但不同公司使用的语言不一样,有 Java 、Python、 php 等等,因此,同一套代码,不同环境,就不适用了;

因此,解决这问题的核心在于封装成api,并且结合GUI界面,做成平台的形式,也就是所谓的 测试数据平台

但目前来看,业界没看到类似开源的例子,可能都是内部使用;

憧憬

虽然创建数据越来越方便了,但每次都需要创建数据,部分可能还是重复数据;

能否创建前先搜索,如果有符合条件的数据,直接返回,没有再创建数据,这样的话,测试数据也会越来越庞大,便于平台化后的数据复用;

不过,这只是想而已,目前来说,jb自认没这能力写搜索逻辑,但一直希望,让自动化更自动;

比如接口测试,可以直接解析接口文档,根据每个字段类型,自动生成数据,这样连数据创建都不需要了;
复制代码

小结

本文主要介绍数据创建相关的内容,大部分在数据创建,有两种方法:

  • 直接使用暴露全部参数的数据准备函数,好处是灵活,弊端是每次调用前都需要准备大量数据;
  • 使用封装函数,会更加灵活,但是可维护性差;

因此会引入 建造者模式 的概念,本质上也是使用api跟操作数据库两种方式来创建数据,只是基于原来的封装再进行二次易用性封装,优点在于业务方可以快速生成需要的数据;

并且介绍了后续平台化的想法,以及个人的一些憧憬;

建造者模式不是万能的,依然对使用场景有限制,用的不好,就会导致易用性差的情况;

温馨提示, 当用例执行完毕,需要把公共数据复原,尽可能减少对其他业务方的干扰

如果需要记录使用的数据,可单独把测试过程的数据入库,以便后面出现问题后有记录复现跟进;

最后,谢谢大家~

JB的测试之旅-测试数据的准备/构造

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Computer Age Statistical Inference

Computer Age Statistical Inference

Bradley Efron、Trevor Hastie / Cambridge University Press / 2016-7-21 / USD 74.99

The twenty-first century has seen a breathtaking expansion of statistical methodology, both in scope and in influence. 'Big data', 'data science', and 'machine learning' have become familiar terms in ......一起来看看 《Computer Age Statistical Inference》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具