mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-10-19 00:13:43 +08:00
mod
This commit is contained in:
231
极客时间专栏/接口测试入门课/进阶技能篇/07 | WebSocket接口:如何测试一个完全陌生的协议接口?.md
Normal file
231
极客时间专栏/接口测试入门课/进阶技能篇/07 | WebSocket接口:如何测试一个完全陌生的协议接口?.md
Normal file
@@ -0,0 +1,231 @@
|
||||
<audio id="audio" title="07 | WebSocket接口:如何测试一个完全陌生的协议接口?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8c/fb/8cd3f08efe6d3b418ae85932636767fb.mp3"></audio>
|
||||
|
||||
你好,我是陈磊。很高兴你又来和我一起探寻接口测试的奥秘了。
|
||||
|
||||
我们在前面一起学习了怎么分析和完成一个HTTP协议的接口测试,又一起学习了如何封装接口测试框架,以及如何搭建接口测试平台。我相信,现在你已经完全掌握了HTTP协议的接口测试了。
|
||||
|
||||
但是,这还不能说明你已经能独立完成接口测试了,为什么这么说呢?这是因为数据在网络传输上都是依靠一种协议来完成的,在上学的时候,你肯定学过包括TCP、UDP、HTTP等在内的一堆协议,但是如果你遇见了一个全新的协议,你知道怎么从零开始,完成接口测试吗?
|
||||
|
||||
今天我就以WebSocket为例,告诉你当你第一次接触一个完全陌生的协议接口时,你要如何开始完成接口测试工作。
|
||||
|
||||
## 未知的新协议接口并不可怕
|
||||
|
||||
作为一名测试工程师,在面对一个陌生协议的接口测试时,你是不是会常常感到很无助?面对这样的任务时,你的第一反应肯定是向开发工程师求助,因为开发工程师基于新协议已经完成了接口开发,向开发工程师求助显然是最好的办法。
|
||||
|
||||
我在工作中就遇见过类似的事情。记得那是在我参加工作的前几年,有一个被测项目的接口是一个私有协议,当我看到接口文档的时候,第一反应就是找开发工程师,向他求教一下这个私有协议。这个开发工程师人很好,他给了我一个学习脉络,其中包含了协议的说明文档、代码开发文档、实现代码等内容,我拿到这些资料后,马上按照上面他给出的学习顺序投入学习。
|
||||
|
||||
但是后来,在项目从交付测试到完成测试后,我发现自己走了一个弯路。因为作为测试工程师,我们不需要了解协议底层的原理,只需要了解新协议是如何传输数据,又如何返回数据库就可以了。也就是说,我们要想模拟一个客户端去验证服务端的逻辑,那么开始接口测试最快速的方法不是去看协议的说明文档,而是直接去看开发实现的客户端代码,这对于我们来说,能更直接地解决问题。但这也并不是说,那位开发工程师告诉我的学习脉络是错误的,只不过它并不是一个非常适合测试工程师的学习方法。
|
||||
|
||||
那在面对一个陌生的新协议时,测试工程师的首要任务是什么呢?
|
||||
|
||||
**在我看来,就是要测试接口的正确逻辑、错误逻辑是否满足最初的需求,因此,我们需要快速地掌握验证手段。**在时间紧迫的情况下,如果我们还是先学习新协议的基础知识,再学习怎么使用它,就无疑压榨了测试的工期,也会让我们在真正开始工作时手忙脚乱。
|
||||
|
||||
所以,我们要从解决实际问题的角度出发,直接拿到开发工程师提供的调用客户端代码,这样我们就可以快速完成工作了;在完成工作的后续时间里,我们也可以慢慢补充基础知识。这里需要你注意的是,我并不是说基础知识不重要,而是说在项目进行过程中,学习基础知识很多时候没有完成项目的质量保障工作重要。
|
||||
|
||||
## 第一次接触WebSocket接口
|
||||
|
||||
我在前面说了一大堆方法论,你看到后可能还是摸不到头脑,那么现在,我就以一个我亲身经历的例子来告诉你,面对一个陌生协议接口要怎么去做测试。
|
||||
|
||||
大概是在2017年,我第一次接触到WebSocket协议的接口,当开发工程师告诉我这是一个WebSocket的接口时,我一脸懵,完全不知道要如何开始测试它。
|
||||
|
||||
我先做的就是和开发要了他们调用方的代码,当我第一次看到这个代码时,还是很难为情的,因为它是用Node.js编写的,当时我对这个技术知之甚少。但凭着自己的经验积累,我多多少少还能看懂一点这个代码,然而我在读了代码之后,发现自己基于这个代码写测试用例并不容易,因为我对Node.js技术实在太陌生了,陌生到我无法利用它来完成接口测试。
|
||||
|
||||
这种情况我相信你肯定也遇见过,那就是开发工程师很Nice地把代码给了你,但你却没办法利用它。但这里我想告诉你的是,面对一个陌生协议的接口测试任务时,无论如何,第一次你还是需要先拿到并了解开发工程师写的客户端代码,因为这样,你就可以对调用方式、参数等接口相关的一些内容有初步印象。在读完相关代码后,你就算是和这些接口完成了首次“会面”,下面你就要想办法敲开接口的大门,让自己能访问被测接口。
|
||||
|
||||
由于技术栈问题,我没办法借助开发工程师的力量完成接口测试任务,因此我接下来想到的是,借助一些自己已经熟悉的工具来完成本次测试。我第一个想到的就是我们在之前课程中一起使用过的Fiddler,因为在任何一个接口项目开始时,无论开发是不是给了我接口文档,我都会先用Fiddler访问看一下。
|
||||
|
||||
那么WebSocket用Fiddler怎么搞定?我当时搜索了一下,还真是有办法,具体的办法我就不在这里多说了,其实主要就是修改了Fiddler中Rules下的Customize Rules,如果你感兴趣可以自己去搜一下。我只是想告诉你,当你面对陌生技术问题的时候,你应该使用你最熟悉的技术去尝试解决问题。
|
||||
|
||||
但从下面的图中你可以看到,虽然我找到了Fiddler截获WebSocket接口的办法,却不难发现,所截获的全部消息都在日志里面,根本无法操作,所以我想用Fiddler完成WebSocket测试的想法也就胎死腹中了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/c5/28455a0b055a49b69f30963c1d3cf8c5.png" alt="">
|
||||
|
||||
但是,我可以借助Fiddler分析WebSocket的接口,这也和我们一开始给Fiddler这款工具的定位一样,那就是通过它辅助分析我们的被测接口。
|
||||
|
||||
## 自己写WebSocket测试代码
|
||||
|
||||
当用已有工具基础解决WebSocket接口测试这个想法破灭了后,我开始寻求通过编写代码,解决WebSocket的接口测试。在这里,我还是建议你要以你自己的技术栈为出发点,寻找解决问题的方法。由于我的主要编程语言是Python,因此下面一些讲解的示例代码段,我还是以Python为例,但是你要知道,解决问题的思路并不限于Python的编程语言,它可以是你使用的任何其它语言。
|
||||
|
||||
我发现Python提供了WebSocket的协议库,因此我只要用它完成客户端的撰写,就可以进行接口测试了。这里,我写下了第一个WebSocket的调用代码(这里我们以 [http://www.websocket.org/demos/echo/](http://www.websocket.org/demos/echo/) 为例),如下面图中所示,我在代码里面写了详细的注释,你肯定能看懂每一句话的意思。
|
||||
|
||||
```
|
||||
#引入websocket的create_connection类
|
||||
from websocket import create_connection
|
||||
# 建立和WebSocket接口的链接
|
||||
ws = create_connection("ws://echo.websocket.org")
|
||||
# 打印日子
|
||||
print("发送 'Hello, World'...")
|
||||
# 发送Hello,World
|
||||
ws.send("Hello, World")
|
||||
# 将WebSocket的返回值存储result变量
|
||||
result = ws.recv()
|
||||
# 打印返回的result
|
||||
print("返回"+result)
|
||||
# 关闭WebSocket链接
|
||||
ws.close()
|
||||
|
||||
```
|
||||
|
||||
不知道你发现没有,上面的代码和HTTP协议的接口类似,都是先和一个请求建立连接,然后发送信息。它们的区别是,WebSocket是一个长连接,因此需要人为的建立连接,然后再关闭链接,而HTTP却并不需要进行这一操作。
|
||||
|
||||
我相信你肯定还记得在测试框架那一节([04](https://time.geekbang.org/column/article/195483))中,我们学习了一些线性的接口测试代码,然后通过分析这些代码抽象出Common类,随着Common类的不断丰富,就形成了你自己私有化的测试框架,那么现在问题来了:Common类中可以也放入WebSocket的通用方法吗?
|
||||
|
||||
## 将WebSocket接口封装进你的框架
|
||||
|
||||
看见上面的代码,我们的第一反应应该是,这里有什么东西可以放到我们自己的Common类中呢?你可以按照“测试代码即框架”这一思路将这个WebSocket接口封装进你的框架。
|
||||
|
||||
我们在前面课程中封装了Common类,你可以在它的构造函数中,添加一个API类型的参数,以便于知道自己要做的是什么协议的接口,其中http代表HTTP协议接口,ws代表WebSocket协议接口。由于WebSocket是一个长连接,我们在Common类析构函数中添加了关闭ws链接的代码,以释放WebSocket长连接。依据前面的交互流程,实现代码如下所示:
|
||||
|
||||
```
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# python代码中引入requests库,引入后才可以在你的代码中使用对应的类以及成员函数
|
||||
import requests
|
||||
from websocket import create_connection
|
||||
|
||||
|
||||
# 定义一个common的类,它的父类是object
|
||||
class Common(object):
|
||||
# common的构造函数
|
||||
def __init__(self,url_root,api_type):
|
||||
'''
|
||||
:param api_type:接口类似当前支持http、ws,http就是HTTP协议,ws是WebSocket协议
|
||||
:param url_root: 被测系统的根路由
|
||||
'''
|
||||
if api_type=='ws':
|
||||
self.ws = create_connection(url_root)
|
||||
elif api_type=='http':
|
||||
self.ws='null'
|
||||
self.url_root = url_root
|
||||
|
||||
|
||||
# ws协议的消息发送
|
||||
|
||||
def send(self,params):
|
||||
'''
|
||||
:param params: websocket接口的参数
|
||||
|
||||
:return: 访问接口的返回值
|
||||
'''
|
||||
self.ws.send(params)
|
||||
res = self.ws.recv()
|
||||
return res
|
||||
|
||||
|
||||
# common类的析构函数,清理没有用的资源
|
||||
|
||||
def __del__(self):
|
||||
'''
|
||||
:return:
|
||||
'''
|
||||
if self.ws!='null":
|
||||
self.ws.close()
|
||||
def get(self, uri, params=None):
|
||||
'''
|
||||
封装你自己的get请求,uri是访问路由,params是get请求的参数,如果没有默认为空
|
||||
:param uri: 访问路由
|
||||
:param params: 传递参数,string类型,默认为None
|
||||
:return: 此次访问的response
|
||||
'''
|
||||
# 拼凑访问地址
|
||||
if params is not None:
|
||||
url = self.url_root + uri + params
|
||||
else:
|
||||
url = self.url_root + uri
|
||||
# 通过get请求访问对应地址
|
||||
res = requests.get(url)
|
||||
# 返回request的Response结果,类型为requests的Response类型
|
||||
return res
|
||||
def post(self, uri, params=None):
|
||||
'''
|
||||
封装你自己的post方法,uri是访问路由,params是post请求需要传递的参数,如果没有参数这里为空
|
||||
:param uri: 访问路由
|
||||
:param params: 传递参数,string类型,默认为None
|
||||
:return: 此次访问的response
|
||||
'''
|
||||
# 拼凑访问地址
|
||||
url = self.url_root + uri
|
||||
if params is not None:
|
||||
# 如果有参数,那么通过post方式访问对应的url,并将参数赋值给requests.post默认参数data
|
||||
# 返回request的Response结果,类型为requests的Response类型
|
||||
res = requests.post(url, data=params)
|
||||
else:
|
||||
# 如果无参数,访问方式如下
|
||||
# 返回request的Response结果,类型为requests的Response类型
|
||||
res = requests.post(url)
|
||||
return res
|
||||
|
||||
|
||||
def put(self,uri,params=None):
|
||||
'''
|
||||
封装你自己的put方法,uri是访问路由,params是put请求需要传递的参数,如果没有参数这里为空
|
||||
:param uri: 访问路由
|
||||
:param params: 传递参数,string类型,默认为None
|
||||
:return: 此次访问的response
|
||||
'''
|
||||
url = self.url_root+uri
|
||||
if params is not None:
|
||||
# 如果有参数,那么通过put方式访问对应的url,并将参数赋值给requests.put默认参数data
|
||||
# 返回request的Response结果,类型为requests的Response类型
|
||||
res = requests.put(url, data=params)
|
||||
else:
|
||||
# 如果无参数,访问方式如下
|
||||
# 返回request的Response结果,类型为requests的Response类型
|
||||
res = requests.put(url)
|
||||
return res
|
||||
|
||||
|
||||
def delete(self,uri,params=None):
|
||||
'''
|
||||
封装你自己的delete方法,uri是访问路由,params是delete请求需要传递的参数,如果没有参数这里为空
|
||||
:param uri: 访问路由
|
||||
:param params: 传递参数,string类型,默认为None
|
||||
:return: 此次访问的response
|
||||
'''
|
||||
url = self.url_root + uri
|
||||
if params is not None:
|
||||
# 如果有参数,那么通过put方式访问对应的url,并将参数赋值给requests.put默认参数data
|
||||
# 返回request的Response结果,类型为requests的Response类型
|
||||
res = requests.delete(url, data=params)
|
||||
else:
|
||||
# 如果无参数,访问方式如下
|
||||
# 返回request的Response结果,类型为requests的Response类型
|
||||
res = requests.put(url)
|
||||
return res
|
||||
|
||||
```
|
||||
|
||||
上面的代码很长,但我的注释很详细,我并不建议你一字不落地都看完,你只要在使用的时候看一下对应的方法就好了。它是一个超级工具集合,最后会变成你自己的类似哆啦A梦的万能口袋,你只要做好自己的积累就可以了。
|
||||
|
||||
那么,使用上述的Common类将上面那个流水账一样的脚本进行改造后,就得出了下面这段代码:
|
||||
|
||||
```
|
||||
from common import Common
|
||||
# 建立和WebSocket接口的链接
|
||||
con = Common('ws://echo.websocket.org','ws')
|
||||
# 获取返回结果
|
||||
result = con.send('Hello, World...')
|
||||
#打印日志
|
||||
print(result)
|
||||
#释放WebSocket的长连接
|
||||
del con
|
||||
|
||||
```
|
||||
|
||||
现在,从改造后的代码中,你是不是更能体会到框架的魅力了?它能让代码变得更加简洁和易读,将WebSocket的协议封装到你的框架后,你就拥有了一个既包含HTTP协议又包含WebSocket协议的接口测试框架了,随着你不断地积累新协议,你的框架会越来越强大,你自己的秘密武器库也会不断扩充,随着你对它的不断完善,它会让你的接口测试工作越来越简单,越来越快速。
|
||||
|
||||
## 总结
|
||||
|
||||
美好的时光过得都很快,又到了本节课结束的时候了,我今天主要讲了面对一个陌生协议时(比如说WebSocket),你该如何从零开始完成接口测试任务。
|
||||
|
||||
针对一个陌生协议的第一次接口测试,你要保持自己敏锐的测试嗅觉,依据自己的技术基础,尽快解决问题。总地来说,你可以通过三步快速完成测试任务:
|
||||
|
||||
1. 借力开发工程师。你首先该借力就是开发工程师,但你不要进入开发工程师给你的那种,从技术基础和理论开始学起,再逐步应用的学习脉络。你要一击致命,直接把他的客户端代码拿来,尽最大可能挪为己用,将其变成自己的接口测试代码。
|
||||
1. 站在自己的技术栈之上,完成技术积累。如果开发工程师的代码并不能拿来使用,那么你就需要站在自己的技术栈上寻求解决方法,这其中既包含了你已经熟悉的测试工具、测试平台,也包含了自己的测试框架和编码基础。
|
||||
1. 归入框架。无论你使用哪一种方法,在完成测试工作后,你还是要掌握对应的理论基础,同时想办法将这个一开始陌生的接口,通过自己熟悉的方式合并到你自己的框架中,不断扩充自己框架的测试能力,不断丰富你自己的测试手段。
|
||||
|
||||
## 思考题
|
||||
|
||||
我们今天一起学习了如何破解陌生协议接口测试难题的步骤,那么面对WebSocket的接口测试任务,结合你现有的技术栈,你是不是也有你自己的解决方案呢?你工作中如果有类似的陌生协议(既可以是第一次接触的协议,也可以是企业私有协议),你是如何解决的呢?欢迎你在留言区中留下你的疑问和你的做法。
|
||||
|
||||
我是陈磊,欢迎你在留言区留言分享你的观点,如果这篇文章让你有新的启发,也欢迎你把文章分享给你的朋友,我们一起沟通探讨。
|
233
极客时间专栏/接口测试入门课/进阶技能篇/08 | 测试数据:是不是可以把所有的参数都保存到Excel中?.md
Normal file
233
极客时间专栏/接口测试入门课/进阶技能篇/08 | 测试数据:是不是可以把所有的参数都保存到Excel中?.md
Normal file
@@ -0,0 +1,233 @@
|
||||
<audio id="audio" title="08 | 测试数据:是不是可以把所有的参数都保存到Excel中?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2d/6e/2d4d9ee7fcc0e9509dc78cfc3c78926e.mp3"></audio>
|
||||
|
||||
你好,我是陈磊。
|
||||
|
||||
课程到现在,我们已经一起从接口测试思维的训练,走到了接口测试技术的训练,随着学习的不断深入,你应该也有了一个自己的测试框架,虽然这个框架可能还很简陋。但是任何事情不管多晚开始,都好于从未开始,因此学到现在,你已经迈出了接口测试以及其测试技术的第一步。
|
||||
|
||||
做任何事情,从零到一都需要莫大的勇气和坚定的决心,在这个过程中,你要将自己挪出舒适区,进入一个陌生的领域,这确实很难。但如果你和我一起走到了这一节课,那么我要恭喜你,你已经完成了接口测试从零到一的转变,后续从一到无穷大,你只需要随着时间去积累经验就可以了。
|
||||
|
||||
如果把接口测试比喻成要炒一盘菜的话,那么我在之前的全部课程中,重点都是在讲解如何完成接口测试,也就是教你如何炒菜。我也教过你如何解决接口测试的需求,为你提供了解决问题的能力和手段,这也就是在帮你建造一个设备齐全的厨房,帮你一起完成接口测试任务 。
|
||||
|
||||
有了精致的厨房后,我也告诉了你要怎么制作顶级的厨具,也就是接口测试的技术方法和实践方式。这些厨具既有锅碗瓢盆,也有刀勺铲叉,这里的锅碗瓢盆就是你的测试框架,刀勺铲叉就是你使用框架完成的测试脚本,这其中既包含了单接口的测试脚本,也包含了业务逻辑多接口测试脚本。
|
||||
|
||||
那么如果想炒菜你还需要准备什么呢?毫无疑问那就是菜,所谓“巧妇难为无米之炊”,即使你有高超的手艺,有世界顶级的厨具,但如果没有做菜的原材料,那也没办法把菜做出来,就算是世界顶级大厨,也无法完成这样的任务。
|
||||
|
||||
今天我就顺着这个思路,和你讲讲菜的准备,也就是接口测试的数据准备工作。
|
||||
|
||||
## 测试数据的好处:打造自动化测试框架
|
||||
|
||||
随着你不断封装自己的测试框架,你的框架就始终处于等米下锅这样一种的状态,而米就是测试数据。我在之前的课程中,都是将测试数据直接写在代码里,赋值给一个变量,然后通过接口测试逻辑来完成测试。
|
||||
|
||||
说到这,我还是把我们之前用过的”战场“这个系统拿出来,看一看它“选择武器”这个接口测试脚本(你可以回到[04](https://time.geekbang.org/column/article/195483)中查看),虽然你现在对怎么撰写、怎么封装类似的接口脚本都已经烂熟于心,但我们还是先看一下它的代码段:
|
||||
|
||||
```
|
||||
# uri_login存储战场的选择武器
|
||||
uri_selectEq = '/selectEq'
|
||||
# 武器编号变量存储用户名参数
|
||||
equipmentid = '10003'
|
||||
# 拼凑body的参数
|
||||
payload = 'equipmentid=' + equipmentid
|
||||
response_selectEq = comm.post(uri_selectEq,params=payload)
|
||||
print('Response内容:' + response_selectEq.text)
|
||||
|
||||
```
|
||||
|
||||
这就是你通过自己的Common类改造后的测试框架,但是现在,它还不能是算是一个完美的框架,为什么呢?
|
||||
|
||||
这是因为,你现在的参数都是直接通过equipmentid变量赋值的,在做测试的时候,你还需要不断修改这个参数的赋值,才能完成接口的入参测试,这不是一种自动化的测试思路。
|
||||
|
||||
因此,你需要将数据封装,通过一种更好的方式,将数据存储到一种数据存储文件中,这样代码就可以自行查找对应的参数,然后调取测试框架执行测试流程,接着再通过自动比对返回预期,检验测试结果是否正确。
|
||||
|
||||
这样做有两个好处。
|
||||
|
||||
1. 无人值守,节省时间和精力。我们将所有的参数都存储到外部存储文件中,测试框架就可以自行选择第一个参数进行测试,在完成第一个测试之后,它也就可以自行选择下一个参数,整个执行过程是不需要人参与的。否则的话,我们每复制一组参数,就要执行一次脚本,然后再人工替换一次参数,再执行一次脚本,这个过程耗时费力,而且又是一个纯人工控制的没什么技术含量的活动。
|
||||
1. 自动检测返回值,提高测试效率。如果你用上面的代码段完成接口测试,就要每执行一次,人工去观察一次,看接口的返回是不是和预期一致,人工来做这些事情,不只非常耗费时间,效率也很低下。但是通过代码完成一些关键匹配却很容易,这可以大大提高测试效率,快速完成交付。
|
||||
|
||||
怎么样,看到这些好处,你是不是也想马上给你的框架加上数据处理的部分了呢?
|
||||
|
||||
## 如何选取测试数据
|
||||
|
||||
现在我们就马上开始动手,为你的框架加上参数类。
|
||||
|
||||
首先,你先要定义一种参数的存储格式。那么我想问你的是,要是让你选择把数据储存在一个文件中,你会选择什么格式的文件呢?
|
||||
|
||||
我相信你肯定和我的选择一样,用Excel。因为目前来看,Excel是在设计测试用例方面使用最多的一个工具,那么我们也就可以用Excel作为自己的参数存储文件。
|
||||
|
||||
但在动手之前,你也应该想到,你的参数文件类型不会是一成不变的Excel,未来你也有可能使用其他格式的参数文件,因此在一开始你还要考虑到参数类的扩展性,这样你就不用每多了一种参数文件存储格式,就写一个参数类,来完成参数的选取和调用了。
|
||||
|
||||
那么如何选取和调用参数呢?你可以看看我设计的参数类:
|
||||
|
||||
```
|
||||
import json
|
||||
import xlrd
|
||||
class Param(object):
|
||||
def __init__(self,paramConf='{}'):
|
||||
self.paramConf = json.loads(paramConf)
|
||||
def paramRowsCount(self):
|
||||
pass
|
||||
def paramColsCount(self):
|
||||
pass
|
||||
def paramHeader(self):
|
||||
pass
|
||||
def paramAllline(self):
|
||||
pass
|
||||
def paramAlllineDict(self):
|
||||
pass
|
||||
|
||||
|
||||
class XLS(Param):
|
||||
'''
|
||||
xls基本格式(如果要把xls中存储的数字按照文本读出来的话,纯数字前要加上英文单引号:
|
||||
|
||||
第一行是参数的注释,就是每一行参数是什么
|
||||
|
||||
第二行是参数名,参数名和对应模块的po页面的变量名一致
|
||||
|
||||
第3~N行是参数
|
||||
|
||||
最后一列是预期默认头Exp
|
||||
'''
|
||||
def __init__(self, paramConf):
|
||||
'''
|
||||
:param paramConf: xls 文件位置(绝对路径)
|
||||
'''
|
||||
self.paramConf = paramConf
|
||||
self.paramfile = self.paramConf['file']
|
||||
self.data = xlrd.open_workbook(self.paramfile)
|
||||
self.getParamSheet(self.paramConf['sheet'])
|
||||
def getParamSheet(self,nsheets):
|
||||
'''
|
||||
设定参数所处的sheet
|
||||
:param nsheets: 参数在第几个sheet中
|
||||
:return:
|
||||
'''
|
||||
self.paramsheet = self.data.sheets()[nsheets]
|
||||
def getOneline(self,nRow):
|
||||
'''
|
||||
返回一行数据
|
||||
:param nRow: 行数
|
||||
:return: 一行数据 []
|
||||
'''
|
||||
return self.paramsheet.row_values(nRow)
|
||||
def getOneCol(self,nCol):
|
||||
'''
|
||||
返回一列
|
||||
:param nCol: 列数
|
||||
:return: 一列数据 []
|
||||
'''
|
||||
return self.paramsheet.col_values(nCol)
|
||||
def paramRowsCount(self):
|
||||
'''
|
||||
获取参数文件行数
|
||||
:return: 参数行数 int
|
||||
'''
|
||||
return self.paramsheet.nrows
|
||||
def paramColsCount(self):
|
||||
'''
|
||||
获取参数文件列数(参数个数)
|
||||
:return: 参数文件列数(参数个数) int
|
||||
'''
|
||||
return self.paramsheet.ncols
|
||||
def paramHeader(self):
|
||||
'''
|
||||
获取参数名称
|
||||
:return: 参数名称[]
|
||||
'''
|
||||
return self.getOneline(1)
|
||||
def paramAlllineDict(self):
|
||||
'''
|
||||
获取全部参数
|
||||
:return: {{}},其中dict的key值是header的值
|
||||
'''
|
||||
nCountRows = self.paramRowsCount()
|
||||
nCountCols = self.paramColsCount()
|
||||
ParamAllListDict = {}
|
||||
iRowStep = 2
|
||||
iColStep = 0
|
||||
ParamHeader= self.paramHeader()
|
||||
while iRowStep < nCountRows:
|
||||
ParamOneLinelist=self.getOneline(iRowStep)
|
||||
ParamOnelineDict = {}
|
||||
while iColStep<nCountCols:
|
||||
ParamOnelineDict[ParamHeader[iColStep]]=ParamOneLinelist[iColStep]
|
||||
iColStep=iColStep+1
|
||||
iColStep=0
|
||||
ParamAllListDict[iRowStep-2]=ParamOnelineDict
|
||||
iRowStep=iRowStep+1
|
||||
return ParamAllListDict
|
||||
def paramAllline(self):
|
||||
'''
|
||||
获取全部参数
|
||||
:return: 全部参数[[]]
|
||||
'''
|
||||
nCountRows= self.paramRowsCount()
|
||||
paramall = []
|
||||
iRowStep =2
|
||||
while iRowStep<nCountRows:
|
||||
paramall.append(self.getOneline(iRowStep))
|
||||
iRowStep=iRowStep+1
|
||||
return paramall
|
||||
def __getParamCell(self,numberRow,numberCol):
|
||||
return self.paramsheet.cell_value(numberRow,numberCol)
|
||||
class ParamFactory(object):
|
||||
def chooseParam(self,type,paramConf):
|
||||
map_ = {
|
||||
'xls': XLS(paramConf)
|
||||
}
|
||||
return map_[type
|
||||
|
||||
```
|
||||
|
||||
上面这个代码看着很多,但你不需要完全看得懂,你只需要知道它解决问题的思路和方法就可以了,**思路就是通过统一抽象,建立一个公共处理数据的方式。**你可以设计和使用简单工厂类的设计模式,这样如果多一种参数存储类型,再添加一个对应的处理类就可以了,这很便于你做快速扩展,也可以一劳永逸地提供统一数据的处理模式。
|
||||
|
||||
如果你的技术栈和我不一样,那么你只需要搜索一下你自己技术栈所对应的简单工厂类设计模式,然后照猫画虎地把上面的逻辑实现一下就可以了。接下来,你就可以把这次测试的全部参数都存到Excel里面了,具体内容如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/5c/93da46d5d04c57a87f0cb6fe38583d5c.jpg" alt="">
|
||||
|
||||
通过上面的参数类你可以看出,在这个Excel文件中,第一行是给人读取的每一列参数的注释,而所有的Excel都是从第二行开始读取的,第二行是参数名和固定的表示预期结果的exp。现在,我们使用ParamFactory类,再配合上面的这个Excel,就可以完成”战场“系统“选择武器”接口的改造了,如下面这段代码所示:
|
||||
|
||||
```
|
||||
#引入Common、ParamFactory类
|
||||
from common import Common
|
||||
from param import ParamFactory
|
||||
import os
|
||||
# uri_login存储战场的选择武器
|
||||
uri_selectEq = '/selectEq'
|
||||
comm = Common('http://127.0.0.1:12356',api_type='http')
|
||||
# 武器编号变量存储武器编号,并且验证返回时是否有参数设计预期结果
|
||||
# 获取当前路径绝对值
|
||||
curPath = os.path.abspath('.')
|
||||
# 定义存储参数的excel文件路径
|
||||
searchparamfile = curPath+'/equipmentid_param.xls'
|
||||
# 调用参数类完成参数读取,返回是一个字典,包含全部的excel数据除去excel的第一行表头说明
|
||||
searchparam_dict = ParamFactory().chooseParam('xls',{'file':searchparamfile,'sheet':0}).paramAlllineDict()
|
||||
i=0
|
||||
while i<len(searchparam_dict):
|
||||
# 读取通过参数类获取的第i行的参数
|
||||
payload = 'equipmentid=' + searchparam_dict[i]['equipmentid']
|
||||
# 读取通过参数类获取的第i行的预期
|
||||
exp=searchparam_dict[i]['exp']
|
||||
# 进行接口测试
|
||||
response_selectEq = comm.post(uri_selectEq,params=payload)
|
||||
# 打印返回结果
|
||||
print('Response内容:' + response_selectEq.text)
|
||||
# 读取下一行excel中的数据
|
||||
i=i+1
|
||||
|
||||
```
|
||||
|
||||
这样再执行你的测试脚本,你就可以看到数据文件中的三条数据,已经都会顺序的自动执行了。那么后续如果将它付诸于你自己的技术栈,以及自己的测试驱动框架比如Python的[unittest](https://docs.python.org/zh-cn/3/library/unittest.html)、Java的[Junit](https://junit.org/junit5/)等,你就可以通过断言完成预期结果的自动验证了。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们接口测试数据准备的内容就到这里了,在接口测试的工作中,作为“巧妇”的测试工程师,还是需要参数这个“米”来下锅的,虽然我们之前课程中的代码涉及到参数的处理,但是都很简单粗暴,一点也不适合自动化的处理方式,因此今天,我带你完成了参数类的封装。
|
||||
|
||||
有的时候,我们也把参数类叫做参数池,这也就是说参数是存放在一个池子中,那我们准备好的池子就是Excel。我相信未来你也会不断扩展自己参数池的种类,这有可能是由于测试接口的特殊需求,也有可能是由于团队技术栈的要求。因此,我们封装参数池是通过简单工厂设计模式来实现的,如果你的代码基础并不好,那么你可以不用搞清楚简单工厂设计模式是什么,只需要知道如何模拟上述代码,再进行扩展就可以了。
|
||||
|
||||
一个好用的测试框架既要有很好的可用性,也要有很好的扩展性设计,这样我们的私有接口测试武器仓库就会变成可以不断扩展的、保持统一使用方法的武器仓库,这样才能让你或者你的团队在面对各种各样的测试任务时,既可以快速适应不同接口测试的需求,又不需要增加学习的成本。
|
||||
|
||||
## 思考题
|
||||
|
||||
今天我们一起学习了参数类的设计,并且将它应用到”战场“系统的接口测试脚本中,后续我又告诉你为了能够完成代码的自动验证,你需要引入一些测试驱动框架,那么,你的技术栈是什么?你在你的框架中选取的测试驱动框架又是什么呢?你能将之前”战场“系统的全流程测试脚本通过参数类完成改造吗?我期待看到你的测试脚本。
|
||||
|
||||
我是陈磊,欢迎你在留言区留言分享你的观点,如果这篇文章让你有新的启发,也欢迎你把文章分享给你的朋友,我们一起沟通探讨。
|
83
极客时间专栏/接口测试入门课/进阶技能篇/09 | 微服务接口:怎么用Mock解决混乱的调用关系?.md
Normal file
83
极客时间专栏/接口测试入门课/进阶技能篇/09 | 微服务接口:怎么用Mock解决混乱的调用关系?.md
Normal file
@@ -0,0 +1,83 @@
|
||||
<audio id="audio" title="09 | 微服务接口:怎么用Mock解决混乱的调用关系?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ec/88/ec6a8f90910428d7c0691858d69eb188.mp3"></audio>
|
||||
|
||||
你好,我是陈磊。
|
||||
|
||||
欢迎你继续和我一起学习接口测试,到目前为止,我们已经学习了接口测试的逻辑模拟,也就是测试辅助工具和测试脚本代码,也学习了如何选取和通过代码调用测试参数,掌握了这些内容,你就算是一个接口测试的老手了。无论你的被测接口是一个你熟悉的协议,还是一个陌生的协议,它们都不会耽误你的工作进度了。
|
||||
|
||||
这节课是我们专栏的最后一节课,我想给你讲一讲关于微服务的接口测试。
|
||||
|
||||
现如今在我的工作中,我主要面对的就是微服务测试,每个服务都是RESTful接口。在最开始的微服务改造过程中,我的测试其实比之前的业务测试更容易,每一个接口通过测试框架来编写测试脚本就可以完成执行了,而且一次写完后再通过平台调用,也显得很轻松。但是这种美好的场景并没有持续多久。为什么呢? 你先听听我的故事。
|
||||
|
||||
## 微服务下混乱的调用关系
|
||||
|
||||
开发团队开始采用微服务架构开发系统的时候,我的测试团队也开始同步学习对应的测试技术,我也像从前一样,逐步封装自己的测试框架,并且采用Postman和Python代码,完成接口测试脚本的快速积累,同时引入了参数类,完成了Excel参数的封装调用。
|
||||
|
||||
在开始的一些项目中,只要开发工程师提交了代码仓库主干的合并请求后,除去代码的静态扫描外,持续集成平台会自动调取一个开源的智能化单元测试框架,来完成单元测试,通过后它会自动部署被测系统,然后再执行测试脚本,这整个流程全部是流水线自动驱动完成的。
|
||||
|
||||
一般来说,开发工程师在开发前期就已经定义好了微服务接口,测试工程师和开发工程师几乎是同步开始进行各自的开发任务。但是,这种和谐的工作场景很快就被蜘蛛网一样的微服务调用关系给破坏了,几乎所有的项目都会出现相互依赖的关系,比如说服务A依赖服务B,服务B依赖服务C,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/bd/aabd0247cbd7c358443ded723d7114bd.jpg" alt="">
|
||||
|
||||
这种混乱主要体现在:
|
||||
|
||||
- 当持续集成流水线部署服务A的时候,由于对应的开发工程师团队也在做同步改造,导致测试环境的服务B不可用;
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/28/9c1e3f58863468fe261553c2dfbe5628.jpg" alt="">
|
||||
|
||||
- 由于服务B依赖服务C,而服务C还没有开发完成,导致即使服务A和服务B都没问题,但也没有办法完成服务A的接口测试。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/90/fbcb82104f467b3968fe05ec3d9c6090.jpg" alt="">
|
||||
|
||||
其实这种服务A依赖服务B,服务B依赖服务C的依赖方式还算简单,还有更多微服务随着开发越来越复杂,服务之间的调用关系就像蜘蛛网一样错乱,让你摸不清外部依赖到底有几层,以及一个接口到底依赖了几个外部接口。
|
||||
|
||||
这就导致了虽然被测系统已经开发完成,测试脚本也准备就绪,但是测试工作就是没办法进行的悲惨结局。面对这种局面,我当时心里确实很不舒服,因为自己做了那么多努力,到头来却被一个不是由自己负责的服务卡住了工作进度,这感觉就像是用尽了全身的力气,却一拳打到了棉花上,自己有再多的劲儿也没处使。
|
||||
|
||||
## Mock框架的抉择:用什么实现服务B的替身
|
||||
|
||||
那作为测试工程师,面对这样的情形,我们该怎么办呢?
|
||||
|
||||
我当时想到的就是使用Mock服务。其实Mock服务是一个错误的说法,关于这一点我推荐你看一下Martin Flower的这篇叫做[TestDouble](https://martinfowler.com/bliki/TestDouble.html)的文章,一般我们将TestDouble服务叫做测试替身,但是如今的国内业界里,绝大部分人已经习惯了叫Mock服务,因此在这里我们也还是叫Mock服务。
|
||||
|
||||
针对混乱的调用关系,我的思路是:我的被测服务就是服务A,那么我不用管服务B是不是好用,我只要保障服务A能够走完流程,就可以完成接口测试任务了。循着这个思路,我只要用Mock服务伪装成服务B就万事大吉了,我也不用再关心服务B到底调用了多少服务。
|
||||
|
||||
但是在选取Mock服务框架时,我又面临着一个抉择,那就是用什么来实现服务B的替身。现在可以实现Mock服务的框架特别多,但绝大部分都要求你有很好的代码基础,每做一个Mock服务其实就是做了一个简单的服务B,不同的是,它不需要实现原有服务B负载的处理逻辑,只要能按服务B的处理逻辑给出对应返回就可以了。
|
||||
|
||||
因此,有些团队也会把这样的服务叫做挡板系统,这个名字很形象。也就是说,我给了Mock服务B的请求参数,它只要按照约定好的返回给我参数就可以了,至于一系列其它验证或者微服务调用,都不在Mock服务的设计内,这就像你对着墙打乒乓球一样,墙是你假设的对手,会把你打过去的球挡回来,而你要做的就是接住墙挡回给你的球。
|
||||
|
||||
那么,到底应该怎么选择Mock服务框架呢?
|
||||
|
||||
首先,你要基于自己和团队的技术栈来做选择,这决定了你完成服务B替身的速度。你要知道,无论服务B的替身做得多么完美,它只是一个Mock,它存在的意义就是帮助你快速完成服务A的接口测试工作,因此,选择一个学习成本低、上手快并且完全适合你自己技术栈的Mock框架,能让你的测试工作事半功倍。
|
||||
|
||||
其次,你要让写好的Mock服务容易修改和维护。Mock服务就是一个在测试过程中替代服务B的替身,就和拍电影时的替身演员一样,替身演员可能有好几个,需要在不同地方拍摄不同的电影片段。Mock服务可能只有一个,也有可能有好几个,为了不同的调用或者测试而存在。但是,Mock服务会随着服务B的变化而变化,如果服务B的请求参数和返回参数有变化,那么Mock服务也要能快速完成修改,并且能马上发挥作用。因此,一个非常容易维护的Mock服务框架,才更能马上快速投入使用,快速发挥作用。
|
||||
|
||||
如果你的团队技术基础很好,开发能力很强,那么我建议你用对应语言的Mock框架,例如Java语言的[Mockito框架](https://github.com/mockito/mockito)和Python语言的[mock框架](https://pypi.org/project/mock/)。
|
||||
|
||||
如果你的团队技术基础相对比较薄弱,那么我推荐你看看[moco](https://github.com/dreamhead/moco),这个框架在开发Mock服务的时候,提供了一种不需要任何编程语言的方式,你可以通过撰写它约束的Json建立服务,并通过命令启动对应的服务,这就可以快速开发和启动运行你需要的Mock服务。
|
||||
|
||||
更重要的是,Json格式的数据文件可以独立完成Mock的服务设计,而且Json的学习成本和Python语言相比,就如同小学一年级的数学和高中数学之间的难度差距一样,就更别说和犹如高等数学的Java语言相比较了。如果你想详细的学习moco,可以直接去它在Github上的项目空间,那里有详细的使用说明和示例代码。
|
||||
|
||||
## 我的Mock服务设计经验
|
||||
|
||||
在选择好Mock框架后,你就可以酣畅淋漓地完成各个外部依赖服务的解耦工作了,但是关于Mock服务,我还想告诉你一些我的设计经验。
|
||||
|
||||
**首先,简单是第一要素。**无论原服务B处理了多么复杂的业务流程,你在设计服务B的Mock服务时,只要关心服务B可以处理几种类型的参数组合,对应的服务都会返回什么样的参数就可以了。这样你就能快速抓住Mock服务B的设计核心,也能快速完成Mock服务B的开发。
|
||||
|
||||
**其次,处理速度比完美的Mock服务更重要。**一个Mock服务要能按照原服务正确又快速地返回参数,你不需要把大量的时间都浪费在Mock服务的调用上,它只是用来辅助你完成接口测试的一个手段。你需要让它像打在墙上的乒乓球一样,一触到墙面马上就反弹回来;而不能把球打出后,需要去喝个茶或者坐下休息一会,才能收到反弹回来的球。
|
||||
|
||||
如果你的Mock服务很耗时,你在只有一个两个服务时,可能影响还不是很明显,但如果你同时有多个Mock服务,或者需要用Mock服务完成性能测试的时候,这就会变成一个很严重的问题,后续会引发强烈的“蝴蝶效应”,使得整个被测接口的响应速度越来越慢。因此你要建立一套快速的Mock服务,尽最大可能不让Mock服务占据系统的调用时间。
|
||||
|
||||
**最后,你的Mock服务要能轻量化启动,并且容易销毁。**你要时刻注意,Mock服务只是一个辅助服务,因此,任何一个团队都不希望启动一个Mock服务需要等待5分钟,或者需要100M的内存。它要能快速启动、容易修改并且方便迁移。既然Mock服务的定位是轻量化的辅助服务,那么它也要容易销毁,以便你在完成测试后,可以快速且便捷地释放它所占据的资源。
|
||||
|
||||
## 总结
|
||||
|
||||
微服务现在已经铺天盖地而来,尤其在中台化战略的推动下,业务中台服务的依赖关系会越来越复杂,并且随着团队内微服务数量越来越多,每个测试团队面临的被测系统都会是一团乱麻,很容易找不到头绪。
|
||||
|
||||
为了解决由于微服务间相互依赖而导致的混乱的系统调用关系,我建议你尽快掌握一个Mock服务框架,这样可以让你在混乱中理清思路,快速进行接口测试,交付高质量的项目。
|
||||
|
||||
最后我要提醒你的是,选择Mock的技术栈与选择测试框架的技术栈还是有些区别的,在选择Mock技术栈时,你重点要考虑的是学习成本,把学习成本降到最低,才是选择Mock框架的首要关注点。而且你不只要关注自己的学习成本,也要关注你所在团队的学习成本,因为现在每个项目都有可能需要Mock服务,这个时候,就要求每一个项目的测试工程师都具备自己独立建设Mock服务的能力,在Mock服务的技术选型上,还是要以团队整体的技术栈为基础,以自己的技术为参考进行选型。
|
||||
|
||||
## 思考题
|
||||
|
||||
这节课我讲了在微服务混乱的外部调用下,使用Mock外部接口完成被测接口的测试工作,文中我也给你推荐了一个快速入门的Mock工具,那么你在工作中,有没有遇见过被测系统因为外部依赖,而不得不阻塞项目进度的时候呢?你又是怎么解决的呢?
|
||||
|
||||
我是陈磊,欢迎你在留言区留言分享你的观点,如果这篇文章让你有新的启发,也欢迎你把文章分享给你的朋友,我们一起沟通探讨。
|
Reference in New Issue
Block a user