理解什么是序列化
在我面试有接口测试经验的候选人的时候,经常会问一个问题:如何对返回的响应结果(假设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内部对象,而且每个版本的实现均有差别。
XML
、JSON
是更流行的序列化协议,因为它们具备了上面提到的两个特性,它们打通了不同语言实现的服务之间的沟通隔阂。
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
单元测试框架。