DataStore使用介绍

在测试框架的data目录下有test.jsontest2.json两份文件,用于存放test、test2环境的静态测试数据,比如:

{
    "id": "581e3432-9f03-0ae3-ddc5-d197601c6850",
    "sn": "00000261001200200019186",
    "name": "自动化测试门店",
    "industry": "e586aa25-312d-11e6-aebb-ecf4bbdee2f0",
    "address": ["江苏省", "苏州市", "姑苏区", "干将路99号"],
    "merchant": "default_merchant",
    "contact_name": "Robot",
    "contact_cellphone": "10088880001",
    "cashiers": ["default_cashier"],
    "admins": ["default_admin"],
    "@object": "store",
    "@extended": {
        "label": "default_store",
        "group": "store"
    }
},
{
    "id": "5c82b3f9-e5d2-4e21-ab4b-800d6c7b7265",
    "sn": "1580000000517499",
    "name": "无锡办事处",
    "industry": "e586aa25-312d-11e6-aebb-ecf4bbdee2f0",
    "address": ["江苏省", "无锡市", "崇安区", "湖滨大道99号"],
    "contact_name": "Bill Smith",
    "contact_cellphone": "10088880002",
    "merchant": "default_merchant",
    "admins": ["alternative_admin"],
    "cashiers": ["alternative_cashier"],
    "@object": "store",
    "@extended": {
        "label": "alternative_store",
        "group": "store"
    }
}

很多同事会把测试中用到的数据改成一个json object并打上一个标签存放到其中,比如:

{
    "username": "10612345005",
    "password": "11111111",
    "account_id": "7e0ce362-1530-497b-922f-54d4ac94beac",
    "merchant_id": "20685e44-1b26-4ed0-a5d6-b14733c55936",
    "store_id": "fabea80e-15db-4fa6-b553-3bd9605ab7a6",
    "store_sn": "21580000000524550",
    "name": "接口测试(勿动)",
    "operator_id": "44b99475-a4b0-49de-bd4b-207e6a3bd815",
    "operator_name": "test",
    "@extended": {
        "label": "default_smart_clound_sound"
    }
}

然后在测试用例中以D.default_smart_clound_sound的方式取到该对象,看上去DataStore仅仅是是负责对json对象的反序列化?

肯定不是

1. 测试数据管理的痛点

测试数据肯定具备了业务语义,所以先来看下主要的一些支付场景下的业务对象:

很多时候,业务对象其实是树状结构,一层层级联下来,在这种结构下很容易设计出这样的测试数据:

{
    "type": "merchant",
    "name": "merchant",
    "stores": [
        {
            "type": "store",
            "name": "store a",
            "terminals": [
                {
                	"type": "terminal",
                	"name": "terminal aa"
                },
                {
                    "type": "terminal",
                    "name": "termial ab"
                }
            ]
        },
        ...
    ]
}

把所有数据放在一个大的json object下,一层层级联下来即可。

然而现实业务对象的关系没有简单,再看一个例子:

这里面AccountUser是两种不同的对象,Account里包含了登录时使用的username以及password字段,使用这两个字段在不同的平台下登录时却是不同的用户:Group UserMerchant User

另外 merchant a又跟merchant user有关联,又属于group hm,如果该对象只允许一次描述,似乎没有办法用json的数据结构来描述了。

与其想尽办法设计出一种数据结构来解决上面的测试数据(对象)层级关系的表达,还不如承认:各个对象就应该是独立的,只是在一些维度上产生了联系

于是剩下的问题就成了『如何让不同的对象的在某个维度上发生联系?』

2. DataStore介绍

hulk.datastore下的DataStore对象负责存储、索引各种测试数据对象(MetaData),它提供多种查找对象的能力:

  • find_obj_by_id:根据对象的id字段查找,复杂度为O(1)
  • find_obj_by_label: 根据对象的标签字段@extended.label查找,复杂度为O(1)
  • find_obj_by_group:根据对象的分组字段@extended.group查找,复杂度为O(1)
  • find_obj: 根据对象的任意字段查找,比如find_obj(name="自动化测试门店", sn="00000261001200200019186")

上面的方法提供了检索对象的能力,然后看下如何建立对象之间的管理

2.1 一对一的关联:

回到第一张对象关系图中,比如Store A,它所属的Merchant肯定是唯一的,这是种一对一的关系,在DataStore中用LinkedAttr来表述这层关系:

class LinkedAttr:
    def __init__(self, attr_name):
        if attr_name == "":
            raise ValueError("disallowed empty attr name")
        self.attr_name = attr_name

    def __get__(self, instance, owner):
        data_store = getattr(instance, datastore_attr_name)
        val = instance.__dict__[self.attr_name]
        for f in (data_store.find_obj_by_id, data_store.find_obj_by_label, data_store.find_obj_by_group):
            ret = f(val)
            if ret is not None:
                return ret

    def __set__(self, instance, value):
        instance.__dict__[self.attr_name] = value

这里不展开对描述符的解释,其中__set__实际了这样的表达式行为store.merchant = "123",而__get__实现了store.merchant这样的行为。

从两个方法的代码实现来看,当我给__set__传入value="123"后,并没有特别魔幻的作用,只是修改了obj_spec的值。

但是在取这个merchant值时(__get__),这个描述符并不是直接返回123,而是去datastore实例中,依次调用find_obj_by_idfind_obj_by_labelfind_obj_by_group去返回具体的对象,也就是说这种一对一的关系,可以通过另外另外对象的idlabelgroup来建立联系,与关系型数据库中的外键概念非常接近。

重新看下最上面的json数据,当我用D.alternative_store.merchant时,实际发生了这样的调用链:find_obj_by_label(alternative_store) -> find_obj_by_label(default_merchant),也就是说D.alternative_store.merchantD.default_merchant实际指向了同一个对象。

2.2 一对多的关联:

从store -> merchant的映射是一对一,但是从merchant -> store的映射却是一对多,这里用另外种描述符来解决这个问题:GroupedAttr

class GroupedAttr(object):

    def __init__(self, group_spec):
        self.data_store = None
        self.group_spec = group_spec

    def __get__(self, instance, owner):
        data_store = getattr(instance, datastore_attr_name)
        ret = []
        for g in instance.__dict__.get(self.attr_name, []):
            for f in (data_store.find_obj_by_id, data_store.find_obj_by_label, data_store.find_obj_by_group):
                s = f(g)
                if s is not None:
                    ret.append(s)
                    break
        return ret

    def __set__(self, instance, value):
        if instance.__dict__.get(self.attr_name, None) is None:
            if isinstance(value, list) or isinstance(value, tuple):
                instance.__dict__[self.attr_name] = value  # value 是list或者tuple
            else:
                instance.__dict__[self.attr_name] = [value]  # value 是string
        else:
            if isinstance(value, list) or isinstance(value, tuple):
                instance.__dict__[self.attr_name].extend(value)
            else:
                instance.__dict__[self.attr_name].append(value)

其实行为与LinkedAttr类似

2.3 namespace

在大部分情况下,我们会利用对象的label去索引,这也要求了在label的值必须是全局唯一的。但当如果多个测试团队维护一份测试数据时,很难保证label不冲突,而且冲突的代价较大:会造成数据的混乱。

namespace可以在用户层面隔离数据,当你在测试数据中添加@extended.namespace字段后,必须要通过D.namespace=<namespace>切换至相应的namespace下才能访问。

默认的namespace为main,而且在main下的对象可以在各个namespace下被访问到。

如果你的测试数据使用了不同的namespace,必须要在用例执行完成后切换回至main,可以利用pytest.fixture来实现。

3. DataFactory介绍:

顾名思义,工厂类,负责将不同自定义json object序列化成Python对象,这层关系转换是通过@object来控制。

比如在hulk.data.__init__.py下,加载了很多自定义的对象:

DataHelper.update_obj_map({
    "merchant": MerchantData,
    "store": StoreData,
    "terminal": TerminalData,
    "account": AccountData,
    "activation code": TerminalActivationCode,
    "vendor": VendorData,
    "vendor_app": VendorAppData,
})

自定义Store对象:

class StoreData(MetaData):
    """门店模型"""
    merchant = LinkedAttr("merchant")
    terminals = GroupedAttr("terminals")
    cashiers = GroupedAttr("cashiers")
    admins = GroupedAttr("admins")

    @classmethod
    def inst_from_dict(cls, d: dict):
        instance = cls()
        terminals = d.pop("terminals", None)
        cashiers = d.pop("cashiers", None)
        admins = d.pop("admins", None)
        for k, v in d.items():
            setattr(instance, k, v)
        if terminals:  
            for terminal in terminals:  # 历史原因
                instance.terminals = terminal
        if cashiers:
            instance.cashiers = cashiers   # 推荐的赋值方法
        if admins:
            instance.admins = admins
        return instance

4. 总结&&建议:

合理、有效的利用LinkedAttrGroupedAttr关联不同对象,可以达到以下目的:

  • 一个业务对象能有完整的字段属性来描述
  • 一个业务对象有且仅会被描述一次,方便管理、维护
  • 可以任意定制、扩展对象间的关系链
  • 大量减少对测试数据的维护
  • 有效的利用namespace来隔离碎片化、低频的对象