Python接口测试实战6 - 序列化与反序列化

理解什么是序列化

在我面试有接口测试经验的候选人的时候,经常会问一个问题:如何对返回的响应结果(假设json格式)中的字段值进行断言?我一般得到的答复是通过正则表达式,有一部分候选人会说使用JSON Path,这就是让我非常困惑的一个地方:好像大部分测试人员都不知道序列化反序列化这种重要的基础概念。

我以一个例子来展开:

class Student:

    def __init__(self, name, age, *courses):
        self.name = name
        self.age = age
        self.courses = courses

mike = Student("Mike Hsu", 20, "English", "Chinese")

Student定义了一个学生的数据结构,有姓名、年龄、课程等属性,mike是其一个实例,从中可以看到,mike全名Mike Hsu,年龄20岁,选修了英文、中文等课程。

然后我导入Python内置的pickle模块,进行一次序列化操作:

import pickle
pickle.dumps(mike)

控制台会打印一串字节数组,如:

b'\x80\x03c__main__\nStudent\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x08\x00\x00\x00Mike Hsuq\x04X\x03\x00\x00\x00ageq\x05K\x14X\x07\x00\x00\x00coursesq\x06X\x07\x00\x00\x00Englishq\x07X\x07\x00\x00\x00Chineseq\x08\x86q\tub.'

你可以尝试复制本地的控制台输出内容,然后执行以下代码:

import pickle
m = pickle.loads(b'xxxxx')   # 替换成你复制的内容
print(m.age)
print(m.name)
print(m.courses)

你会惊奇的发现,m这个对象的值与上面的mike值是一致的,即便这两个对象存在于两次运行之中。

参照维基百科的解释,结合上面的例子,我们来理解下到底什么是序列化和反序列化

计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。

从一系列字节提取数据结构的反向操作,是反序列化

JSON是种序列化协议

pickle是Python内置的序列化模块,用于对Python对象的序列化以及反序列化,但是应用并不广泛——序列化协议往往要求具备跨语言、跨平台特性,它仅仅用于python内部对象,而且每个版本的实现均有差别。

XMLJSON是更流行的序列化协议,因为它们具备了上面提到的两个特性,它们打通了不同语言实现的服务之间的沟通隔阂。

Python也内置的json的序列化库,使用上非常简单方便:

import json
j = '{"name": "Mike Hsu", "age": 20, "courses": ["English", "Chinese"]}'
jo = json.loads(j)

调用json.loads会将json字符串序列化成Python的Dict或者List对象。

在接口测试中,我们应该将数据对象与服务调用传输的字节流解耦:

  • 调用传输层面应用到的是序列化后的二进制数据
  • 用例层面使用的应该是基于二进制数据反序列化后的数据对象
import json


class Student:

    def __init__(self, name=None, age=None, *courses):
        self.name = name
        self.age = age
        self.courses = courses

    def json_encode(self):
        return json.dumps(self.__dict__)

    @classmethod
    def json_decode(cls, data: bytes):
        d = json.loads(data)
        ins = cls()
        for k, v in d.items():
            setattr(ins, k, v)
        return ins

requests对json的支持

JSON Object与Python的Dict有非常高的相似度,所以往往我们更习惯使用Dict来表示基于json的数据对象,另外在requests这个类库中对json有非常高的支持度:

如一个接口的请求报文格式如下:

{
  "name": "Mike Hsu",
  "age": 20
}

使用requests可以这样发送:

import json
mike = {"name": "Mike Hsu", "age": 20}  # 注意此处是dict,并非json
requests.post("http://example.com/api", data=json.dumps(mike), headers={"Content-Type": "application/json"})

但更简单的办法是:

requests.post("http://exmaple.com/api", json=mike)

即使用json这个入参就自行完成了对象的序列化以及设置请求头中的Content-Type

再回到上一节我们封装的RedmineClient,我们可以这么修改:

class RedmineClient:

    def __init__(self, base_url, api_key=None):
        self.base_url = base_url  # 服务入口地址,不同环境入口地址不同
        self.api_key = api_key
        self.session = requests.Session()

    def list_issue(self, **kwargs):
        return self.session.get("{}/issues.json".format(self.base_url), params=kwargs, headers={"X-Redmine-API-Key": self.api_key}).json()

    def get_issue(self, issue_id, **kwargs):
        return self.session.get("{}/issues/{}.json".format(self.base_url, issue_id), params=kwargs, headers={"X-Redmine-API-key": self.api_key}).json()

我们调用其中一个接口,获取内部的一个字段值:

client = RedmineClient("http://redmine.xuh.me", "490592ea4e46348df29828edd597acbfdc5ebb4a")
res = client.get_issue("1")
print(res["issue"]["id"])

这样是不是远比使用正则表达式提取字符串内的值简单方便的多?


对于测试团队而言,需要充分了解接口层面使用的序列化协议,如果序列化协议有跨语言特性,在做接口测试时使用的技术栈其实就不会受限于开发的技术栈了,比如我还可以顺手改成golang的技术栈:

下一讲我们会开始介绍unittest单元测试框架。