第3章接口自动化测试
接口自动化测试是集成自动化测试的主要体现。在实际项目中,较流行的接口是基于HTTP的REST接口和基于RPC的Dubbo接口。
3.1基础知识
3.1.1HTTP和REST
HTTP的全称为Hypertext Transfer Protocol,即超文本传输协议。在TCP/IP分层模型中,HTTP属于应用层协议。HTTP采用CS(Client/Server,客户端/服务端)模型,即客户端连接服务端发送请求后,需要等待直到收到服务端的响应。另外,HTTP是无状态的,即后续的请求如果需要用到前面的数据(状态),那么必须重传。
HTTPS是HTTP通过SSL/TLS加密而成的。与HTTP相比,HTTPS的安全性更高,但传输速度更慢。
1. HTTP消息体
以访问笔者官网(网址详见前言二维码)为例来看一个HTTP消息的结构,HTTP请求的消息如下:
GET http://www.lujiatao.com/ HTTP/1.1
Host: www.lujiatao.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
HTTP响应的消息如下:
HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 05 May 2021 07:34:28 GMT
Content-Type: text/html
Content-Length: 895
Last-Modified: Sun, 14 Mar 2021 08:04:52 GMT
Connection: keep-alive
ETag: "604dc3a4-37f"
Accept-Ranges: bytes
卢家涛
HTTP的请求消息和响应消息具有类似的结构,它们包括以下三部分。
(1) 请求行/响应行: HTTP请求消息的第一行被称为起始行或请求行,它包括请求方法(以上示例的GET)、请求URL和HTTP版本(以上示例的HTTP/1.1)。HTTP响应消息的第一行被称为状态行或响应行,它包括HTTP版本(以上示例的HTTP/1.1)、状态码(以上示例的200)和状态文本(以上示例的OK)。
(2) 请求头/响应头: HTTP请求头/响应头即请求消息/响应消息中的Headers,它们位于第一行之后和空行之前,并以“名称: 值”对的形式出现。对于以上示例,Host: www.lujiatao.com到AcceptLanguage: zhCN,zh;q=0.9,en;q=0.8之间的所有内容都是请求头,而Server: nginx/1.19.6到AcceptRanges: bytes之间的所有内容都是响应头。
(3) 请求体/响应体: HTTP请求体/响应体即请求消息/响应消息中的Body,它们位于空行之后。对于以上示例,请求体是空的(即空行之后没有内容),而响应体是一个HTML文本。
2. HTTP请求方法
HTTP请求方法用于描述对给定资源执行的期望操作,包括以下9种请求方法。
(1) GET: GET方法用于获取指定资源,可理解为“读取”资源。在GET方法的URL中可以携带参数。
(2) HEAD: 与GET方法一样,HEAD方法也用于获取指定资源,但HEAD方法的响应消息没有响应体。
(3) POST: POST方法用于创建指定资源,比如常见的提交表单或上传文件等操作。POST方法既可以在URL中携带参数,也可以在请求体中携带数据。
(4) PUT: PUT方法用于修改指定资源,通常是将资源进行整体替换。PUT方法是幂等的,即同样的请求调用一次与调用多次的效果是一样的。
(5) PATCH: PATCH方法也用于修改指定资源,但通常是将资源进行局部修改。PATCH方法和POST方法一样,它们都是非幂等的。
(6) DELETE: DELETE方法用于删除指定的资源。
(7) OPTIONS: OPTIONS方法一般用于检测服务器支持的请求方法。在OPTIONS方法的响应消息中包含一个名为Allow的响应头,该响应头的值表示了服务器支持的HTTP方法。
(8) TRACE: TRACE方法主要用于调试或测试,是对服务器的一种连通性测试方法。
(9) CONNECT: CONNECT方法一般用于代理服务器。比如目标服务器使用了HTTPS进行数据传输,若客户端(比如浏览器)使用代理服务器,那么客户端会首先使用CONNECT方法向代理服务器发送目标服务器的IP地址、端口和身份认证信息,在代理服务器与目标服务器建立连接后再进行后续的数据传输。
3. HTTP状态码
由于HTTP状态码存在于HTTP响应消息中,因此又被称为HTTP响应码。
HTTP状态码由3位数字组成,其中第一位数字代表当前响应的类型,共包含5种类型。
(1) 1××: 表示一些提示性的响应信息。这类响应通常无须过多关注。
(2) 2××: 表示请求成功。例如,200 OK(请求成功)、201 Created(请求成功并创建了一个资源)、204 No Content(请求成功但响应体为空)。
(3) 3××: 表示重定向。例如,302 Found(资源被重定向到其他地址)、304 Not Modified(资源未修改,客户端应使用缓存)。
(4) 4××: 表示客户端错误。例如,400 Bad Request(请求的语义或参数错误)、401 Unauthorized(未进行身份认证)、403 Forbidden(禁止访问资源)、404 Not Found(请求的资源不存在)、405 Method Not Allowed(不支持的请求方法)。
(5) 5××: 表示服务器错误。例如,500 Internal Server Error(服务器发生了无法处理的内部错误)、502 Bad Gateway(网关无法得到应用服务器的正确响应)、503 Service Unavailable(服务器还未准备好处理该请求)、504 Gateway Timeout(网关等待应用服务器的响应超时)。
4. REST和REST接口
(1) REST。
REST(Representational State Transfer,表现层状态转换)是使用了HTTP子集的一种软件架构风格,其被设计用于代替SOAP(Simple Object Access Protocol,简单对象访问协议)。在这种架构风格中,用户通过资源标识符及对资源的操作(如GET、POST等)使资源的表现形式(状态)被转换,并呈现给最终用户以供使用。REST最初源于Roy T. Fielding在2000年发表的博士论文Architectural Styles and the Design of Networkbased Software Architectures,有兴趣的读者可查阅该论文。
(2) REST接口。
本节的重点聚焦到REST接口上。
REST接口是指遵循REST架构风格的接口,这种接口也被称为RESTful接口或RESTful API。
如果服务器的主域名为www.example.com,那么REST接口的基URL一般为http(s)://api.example.com或http(s)://www.example.com/api。考虑到接口的更新换代,一般还需要加入接口的版本(如v1、v2等),此时REST接口的URL演变为http(s)://api.example.com/v1或http(s)://www.example.com/api/v1。
接着需要在URL中体现要操作的资源。资源应该使用名词来表示,且由于资源可以有多个,因此通常使用名词的复数形式来表示资源。例如,/users表示用户资源,/users/1表示用户ID为“1”的用户资源。加入资源后,REST接口的完整URL已经被构建完成,完整URL如下:
http(s)://api.example.com/v1/users
http(s)://api.example.com/v1/users/1
或者如下:
http(s)://www.example.com/api/v1/users
http(s)://www.example.com/api/v1/users/1
在REST接口中,对资源的操作需要使用部分HTTP请求方法来表示,针对不同的操作对应的HTTP请求方法如下所述。
① 获取资源: 使用GET请求,服务器应该返回0个、1个或多个资源。
② 创建资源: 使用POST请求,指示服务器创建一个资源。
③ 修改资源: 使用PUT或PATCH请求,PUT请求用于整体修改资源,而PATCH请求同于局部修改资源。
④ 删除资源: 使用DELETE请求,指示服务器删除一个资源。
REST接口的7个完整示例如表31所示。
表31REST接口的7个完整示例
请 求 示 例说明响应体示例状态码
GET /users
查询全部用户,服务器应该返回一个用户列表
[{
"id": 1,
"idCard": "510105199612278838",
"name": "张三"
}, {
"id": 2,
"idCard": "510105199612276891",
"name": "李四"
}, {
...
}]200续表
请 求 示 例说明响应体示例状态码
GET /users/1
查询用户ID为1的用户,服务器应该返回一个用户
{
"id": 1,
"idCard": "510105199612278838",
"name": "张三"
}
200
GET
/users?page=2&per_page=10
根据过滤条件查询用户,服务器应该返回一个用户列表
[{
"id": 11,
"idCard": ...,
"name": ...
}, {
...
}, {
"id": 20,
"idCard": ...,
"name": ...
}]
200
POST /users
{
"idCard": "510105199612278838",
"name": "张三"
}
创建一个用户,服务器应该返回该创建的用户
{
"id": 1,
"idCard": "510105199612278838",
"name": "张三"
}
201
PUT /users/1
{
"idCard": "51010519961227883X",
"name": "张三(新)"
}
整体修改一个用户,服务器应该返回修改后的用户
{
"id": 1,
"idCard": "51010519961227883X",
"name": "张三(新)"
}
200
PATCH /users/1
{
"name": "张三(新)"
}
局部修改一个用户,服务器应该返回修改后的用户
{
"id": 1,
"idCard": "510105199612278838",
"name": "张三(新)"
}
200
DELETE /users/1
删除一个用户,服务器应该返回为空204
在实际项目中,通常还会将数据封装到data中,并提供code和message字段以增加返回数据的实用性。比如重新封装GET /users/1请求的响应体,内容如下:
{
"code": 1,
"message": "成功",
"data": {
"id": 1,
"idCard": "510105199612278838",
"name": "张三"
}
}
3.1.2RPC和Dubbo
1. RPC
RPC(Remote Procedure Call,远程过程调用)协议是计算机通信协议,它允许本地计算机上的应用程序调用远程计算机上的应用程序,且这个远程调用过程类似于本地调用。RPC的优势在于为开发人员屏蔽了远程调用的底层技术细节,让开发人员将精力聚焦于上层业务逻辑。在面向对象编程的应用程序中,RPC通常体现为RMI(Remote Method Invocation,远程方法调用)。RPC有很多具体的实现形式,常见的有如下5种。
(1) NFS(Network File System,网络文件系统)。NFS被设计用于跨计算机、操作系统、网络结构和传输协议的文件共享,这种可移植性底层是基于RPC来实现的。
(2) Java RMI(Java Remote Method Invocation,Java远程方法调用)。Java RMI允许在一个JVM中运行的应用程序调用另一个JVM中运行的应用程序提供的方法,因此它实现了Java应用程序之间的远程通信功能。
(3) JSONRPC(JavaScript Object NotationRemote Method Invocation,JavaScript对象表示法远程方法调用)。JSONRPC是一种无状态、轻量级的RPC实现,它使用JSON作为数据格式。
(4) XMLRPC(Extensible Markup LanguageRemote Method Invocation,可扩展标记语言远程方法调用)。 XMLRPC是使用XML作为数据格式、HTTP作为传输机制的RPC实现。
(5) Apache Dubbo(以下简称为Dubbo)。其是一个高性能、轻量级的开源Java微服务框架。
2. Dubbo和Dubbo接口
Dubbo是一个高性能、轻量级的开源Java微服务框架,它是一个RPC的实现框架。Dubbo提供了以下六大核心能力。
(1) 面向接口代理的高性能RPC调用: 基于代理的高性能远程调用能力,服务以接口为粒度,为开发人员屏蔽了远程调用的底层细节。
(2) 智能负载均衡: 内置了多种负载均衡策略,可智能感知下游节点的健康状况,从而显著减少调用延迟,以提高系统的吞吐量。
(3) 服务自动注册及发现: 支持多种类型的注册中心,可实时感知服务实例的上下线。
(4) 高度可扩展能力: 遵循“微内核+插件”的设计原则,所有核心能力如协议、网络传输及序列化等被设计为扩展点,平等对待内置实现和第三方实现。
(5) 运行期间流量调度: 内置条件、脚本等路由策略,通过配置不同的路由策略,可轻松实现灰度发布、同机房优先等功能。
(6) 可视化的服务治理与运维: 提供丰富的服务治理和运维工具——随时查询服务元数据、服务健康状态及调用统计,并可以实时下发路由策略及调整配置参数。
Dubbo的基本架构如图31所示。
图31Dubbo的基本架构
注: 虚线表示初始化; 实线表示同步调用; 点线表示异步调用
首先,由容器启动服务提供者,并将服务提供者注册到注册中心,即向注册中心声明可以提供的服务。然后,服务消费者向注册中心请求需要使用的服务,注册中心将可用的服务返回给服务消费者。由于当可用的服务发生变化时,注册中心需要通知服务消费者,因此服务消费者对于注册中心来说是一个订阅者的角色,而注册中心对于服务消费者来说是一个发布者的角色。最后,服务消费者调用服务提供者提供的服务完成应用程序的请求。对于监控中心而言,需要同时统计服务消费者和服务提供者的调用信息(包括调用次数和调用时间)。另外,注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。
以上都是对Dubbo框架的介绍,那么Dubbo接口又是什么呢?在Dubbo框架中,服务提供者将接口暴露为可远程调用的服务,因此Dubbo接口本质上就是Java接口,只是这些接口需要通过Dubbo框架暴露给服务消费者以供调用。
3.2查看接口的辅助工具
3.2.1浏览器开发者工具
使用开发者工具可以方便地看到当前浏览器中的HTTP请求和响应数据,提供开发者工具的主要浏览器包括Chrome、Edge、Safari和Firefox等。
以Chrome为例,在Windows计算机中使用F12键或Ctrl+Shift+I组合键打开开发者工具在Network标签页可看到HTTP请求和响应数据,如图32所示。
图32使用Chrome开发者工具查看HTTP请求数据
说明如果读者使用的是笔记本计算机,通常就需要使用fn+F12组合键才能打开开发者工具。另外,在macOS计算机中,需要将Ctrl+Shift+I组合键换为option(alt)+command+I组合键。
3.2.2HTTP代理和调试工具
浏览器开发者工具只能针对Web网页发起的HTTP请求进行数据展示,其无法显示手机端发起的HTTP请求数据。因此如果需要查看多个端(如Web网页、手机等)的HTTP请求数据,需要使用专业的HTTP代理工具,如Fiddler、Charles和Burp Suite等。这类工具通常还带有调试功能,如拦截请求、篡改响应等。
目前,Fiddler已经分为Fiddler Everywhere、Fiddler Classic、Fiddler Jam、FiddlerCap和FiddlerCore 5个版本,本节以Fiddler Everywhere为例介绍如何查看HTTP请求数据。
首先,访问Fiddler官方网站下载Fiddler Everywhere(地址详见前言二维码)。
说明下载Fiddler Everywhere时需填写邮箱地址并选择国家,还需要选择I accept the Fiddler End User License Agreement选项,读者按照自身情况填写即可。
下载后是一个.exe安装文件,直接双击安装即可。安装完成后,Fiddler Everywhere会自动打开并要求登录,读者可使用已有账户或创建新账户来进行登录。
如果使用Fiddler Everywhere查看Web网页的HTTP请求数据,就不需要额外的配置操作,直接在浏览器中访问网页即可在Fiddler Everywhere中查看到数据,如图33所示。
图33使用Fiddler Everywhere查看HTTP请求数据
由于本节并不是介绍如何使用Fiddler Everywhere查看HTTP请求数据的详细指南,因此如果读者需要使用它来查看HTTPS请求数据或手机的HTTP(S)请求数据,需要进行额外配置。配置方法可查阅官方文档,查看HTTPS请求数据的配置方法官方文档地址详见前言二维码。
另外,查看Android和iOS设备HTTP(S)请求数据的配置方法也请参考官方文档,官方文档地址详见前言二维码。
3.3使用Requests测试HTTP接口
在Python中,Requests是最流行的HTTP函数库,本节将介绍使用Requests来对HTTP接口进行测试。
Requests支持Python 2.7/3.5+及PyPy,其主要特性如下所述。
(1) 自动创建连接池和设置Connection为keepalive。
(2) 将Cookie通过“键值”对的形式进行持久化,以便在多个请求之间共用会话。
(3) 类似于浏览器的SSL/TLS验证机制。
(4) 自动进行内容的解码和解压缩。
(5) 支持Basic和Digest身份认证机制。
(6) Unicode编码的响应体。
(7) 支持HTTP和HTTPS代理。
(8) 支持Multipart类型的文件上传,支持流媒体下载。
(9) 支持设置请求超时时间。
(10) 支持分块传输。
本节会使用到两个示例: Web应用程序httpbin.org和IMS(Inventory Management System,库存管理系统)。前者由Requests的作者Kenneth Reitz提供,后者是笔者开发的一个用于学习自动化测试的项目。
3.3.1简单请求和响应
在使用Requests之前需要先安装它,安装命令如下:
pip install requests
在chapter_03包中新增learning_requests子包,在learning_requests子包中新增simple_request_and_response模块,然后编写一些简单的请求。
【例31】编写一些简单的请求。
import requests
response_01 = requests.get('https://httpbin.org/get')
response_02 = requests.post('https://httpbin.org/post')
response_03 = requests.put('https://httpbin.org/put')
response_04 = requests.patch('https://httpbin.org/patch')
response_05 = requests.delete('https://httpbin.org/delete')
print(response_01.request.method, response_02.request.method, response_03.request.method, response_04.request.method,response_05.request.method)
以上代码在导入了requests模块后,直接调用其中的函数便可以执行GET、POST、PUT、PATCH和DELETE请求。在Requests中,HTTP响应被表示为一个Response对象,访问request属性可以得到响应的请求信息。以上代码在获得该请求信息后从中又提取了HTTP请求方法。
执行以上代码,执行结果如下:
GET POST PUT PATCH DELETE
当然还可以从Response对象中获取状态码、响应头和响应体。比如提取response_01状态码、响应头和响应体,代码如下:
print(f'状态码:{response_01.status_code}')
print(f'响应头:{response_01.headers}')
print(f'响应体:{response_01.json()}')
执行结果如下:
状态码:200
响应头:{'Date': 'Mon, 10 May 2021 22:50:42 GMT', 'Content-Type': 'application/json', 'Content-Length': '307', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true'}
响应体:{'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.1', 'X-Amzn-Trace-Id': 'Root=1-6099b8c2-26e8ec416670ffff528aaee5'}, 'origin': '222.209.71.15', 'url': 'https://httpbin.org/get'}
从以上代码和输出可以看出,分别访问Response对象的status_code和headers属性可以获取状态码和响应头,而调用Response对象的json()方法可以获取响应体的JSON表示形式。
除了调用Response对象的json()方法,还可以访问它的text属性以直接获取响应体的文本表示形式,代码如下:
print(response_01.text)
执行结果如下:
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.25.1",
"X-Amzn-Trace-Id": "Root=1-6099bd5b-3ce8749f1aad443f3528ca14"
},
"origin": "222.209.71.15",
"url": "https://httpbin.org/get"
}
说明,Requests会自动解码服务器的响应内容,但有时候不一定能处理正确,可以自己手动处理编码问题,代码如下:
response.encoding = 'utf-8'
print(response.text)
以上代码首先把响应内容设置为UTF8编码,然后再获取响应体。
3.3.2构建请求参数
1. 构建URL参数
URL参数常用于GET请求的参数携带。
新增build_request_param模块,编写构建URL参数的代码。
【例32】编写构建URL参数的代码。
import requests
response = requests.get('https://httpbin.org/get?key=value')
print(response.json())
以上代码直接将URL参数添加在URL后面,这也是使用浏览器直接访问GET请求的方式。执行结果如下:
{'args': {'key': 'value'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.1', 'X-Amzn-Trace-Id': 'Root=1-6099c6ca-2f24cd593d9b6d486486f5b9'}, 'origin': '222.209.71.15', 'url': 'https://httpbin.org/get?key=value'}
从以上输出结果可以看出,httpbin.org对带URL参数的请求做了特殊处理,即将请求传入的参数用args原样返回给调用者。
虽然可以直接将URL参数添加到URL中,但是更通用的方式是使用params参数将URL参数添加到请求中,代码如下:
my_params = {
'key': 'value'
}
response = requests.get('https://httpbin.org/get', params=my_params)
此时可以调用Response对象的url属性查看构建后的URL,代码如下:
print(response.url)
执行结果如下:
https://httpbin.org/get?key=value
从以上代码和输出可以看出,使用params参数构建的URL参数与直接将URL参数添加到URL后面的效果是一样的。不过推荐使用params参数的形式,这样便于将URL与参数解耦,方便自动化测试用例的维护。
2. 构建请求体参数
以POST请求为例介绍请求体参数的构建。常见的构建请求体参数有使用表单和JSON两种方式。
如果使用表单方式构建请求体参数,那么需要将数据传递给data参数,代码如下:
my_datas = {
'key': 'value'
}
response = requests.post('https://httpbin.org/post', data=my_datas)
print(response.json())
执行结果如下:
{'args': {}, 'data': '', 'files': {}, 'form': {'key': 'value'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '9', 'Content-Type': 'application/x-www-form-urlencoded', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.1', 'X-Amzn-Trace-Id': 'Root=1-6099cbc1-1257865c33309bfd7ea2eeac'}, 'json': None, 'origin': '222.209.71.15', 'url': 'https://httpbin.org/post'}
从以上输出结果可以看出,httpbin.org对表单参数也做了特殊处理,即将请求传入的参数用form原样返回给调用者。
如果使用JSON方式构建请求体参数,那么需要将数据传递给json参数,代码如下:
my_json = {
'key': 'value'
}
response = requests.post('https://httpbin.org/post', json=my_json)
print(response.json())
执行结果如下:
{'args': {}, 'data': '{"key": "value"}', 'files': {}, 'form': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '16', 'Content-Type': 'application/json', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.1', 'X-Amzn-Trace-Id': 'Root=1-6099ccde-7a06a7b213cb935122e1da43'}, 'json': {'key': 'value'}, 'origin': '222.209.71.15', 'url': 'https://httpbin.org/post'}
httpbin.org将json参数用data原样返回给了调用者。
3. 自定义请求头
除了常见的URL形式或请求体形式的参数携带,有时需要将参数以自定义请求头的方式传递,例如,传递一个名称为key,值为value的请求头,代码如下:
my_headers = {
'key': 'value'
}
response = requests.post('https://httpbin.org/post', headers=my_headers)
print(response.json())
执行结果如下:
{'args': {}, 'data': '', 'files': {}, 'form': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '0', 'Host': 'httpbin.org', 'Key': 'value', 'User-Agent': 'python-requests/2.25.1', 'X-Amzn-Trace-Id': 'Root=1-6099ce05-0702cbd92292679d2faf7199'}, 'json': None, 'origin': '222.209.71.15', 'url': 'https://httpbin.org/post'}
httpbin.org将自定义的请求头附加到headers中,并返回给调用者。
说明请求头的名称对字母的大小写不敏感,如UserAgent和useragent表示的是一样的。
以上示例中的返回值都是经过httpbin.org特殊处理的,实际项目的返回值肯定是不同的,这点需要读者特别注意。另外,对于文件上传的参数构建会有所区别,详见3.3.6节。
3.3.3操作Cookie
客户端(比如浏览器)在与服务器的交互过程中,为了验证客户端的合法性,当客户端与服务器建立连接并认证通过后,服务器会将会话ID放在响应头的SetCookie中,客户端在获取会话ID后,在后续请求中将会话ID通过Cookie的形式携带并发送回服务器。
在Requests中,访问Response对象的cookies属性可以获取Cookie,而Cookie由一个RequestsCookieJar对象表示。新增operate_cookie模块,并以登录IMS为例演示Cookie的获取。
【例33】获取登录IMS的Cookie。
import requests
from requests.cookies import RequestsCookieJar
body = {
'username': 'admin',
'password': 'admin123456'
}
response = requests.post('http://ims.lujiatao.com/api/login', data=body)
cookie = response.cookies
assert isinstance(cookie, RequestsCookieJar)
print(f'Cookie:{cookie.items()}')
执行以上代码,执行结果如下:
Cookie:[('JSESSIONID', 'BE96D3C41CA7644F438C5B1AC07439D5')]
以上代码调用了RequestsCookieJar对象的items()方法来获取全部Cookie。
说明会话ID是由服务器根据一定算法生成的,因此读者看到的会话ID会与笔者的不同。
当然也可以只获取Cookie的全部键或全部值,分别使用keys()和values()方法即可,代码如下:
print(f'Cookie Keys:{cookie.keys()}')
print(f'Cookie Values:{cookie.values()}')
执行结果如下:
Cookie Keys:['JSESSIONID']
Cookie Values:['BE96D3C41CA7644F438C5B1AC07439D5']
还可以获取指定的Cookie值,代码如下:
print(f'会话ID:{cookie.get("JSESSIONID")}')
执行结果如下:
会话ID:BE96D3C41CA7644F438C5B1AC07439D5
以上代码是调用get()方法来获取指定的Cookie值,也可以直接以字典形式来访问,代码如下:
print(f'会话ID:{cookie["JSESSIONID"]}')
一旦获取到Cookie后,就可以在后续请求中将Cookie作为cookies参数传递给其他请求了,比如登录IMS后获取物品分类,代码如下:
requests.get('http://ims.lujiatao.com/api/goods-category', cookies=cookie)
当然在获取了Cookie后,也可对Cookie进行修改后再使用。修改Cookie需要使用RequestsCookieJar对象的set()方法,代码如下:
cookie.set('key', 'value')
print(f'新Cookie:{cookie.items()}')
执行以上代码,执行结果如下:
新Cookie:[('key', 'value'), ('JSESSIONID', 'BE96D3C41CA7644F438C5B1AC07439D5')]
当Cookie作为cookies参数传递时,可使用字典来代替RequestsCookieJar对象,代码如下:
dict_cookie = {
'JSESSIONID': cookie.get('JSESSIONID')
}
requests.get('http://ims.lujiatao.com/api/goods-category', cookies=dict_cookie)
3.3.4详解request()函数
由于requests模块的get()、post()、put()、patch()、delete()、head()和options()函数都是对request()函数的进一步封装,因此有必要详细了解request()函数的使用。
在3.3.1节中,笔者使用get()函数向httpbin.org发起了一个GET请求,代码如下:
requests.get('https://httpbin.org/get')
但也可直接使用request()函数来替代get()函数,代码如下:
requests.request('get', 'https://httpbin.org/get')
从以上代码可以看出,通过将请求方法以字符串形式传递给request()函数的第一个参数可以替代get()函数。同理,也可以使用同样方式替代post()、put()、patch()、delete()、head()和options()函数。
request()函数支持许多参数,但除了method和url外,其他都是非必填参数。具体详见表32。
表32request()函数参数
参 数 名 称参 数 含 义必填
methodHTTP请求方法,支持GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS和TRACE。对大小写不敏感,即传入get、Get或GET都表示GET请求是
url请求的URL是
paramsURL查询参数,通常使用字典类型,也可使用列表(列表元素为元组)等否
data请求体,通常使用字典类型,也可使用列表(列表元素为元组)等。ContentType默认为application/xwwwformurlencoded否续表
参 数 名 称参 数 含 义必填
json请求体,通常使用字典类型,也可使用可序列化的Python对象。ContentType默认为application/json
否
headers请求头,使用字典类型否
cookiesCookie,使用字典类型或CookieJar类型对象。当然也可以使用RequestsCookieJar类型对象,因为RequestsCookieJar是CookieJar的子类否
files待上传的文件,ContentType为multipart/formdata。详见3.3.6节否
auth身份认证数据,可使用元组类型或可调用对象,如('username', 'password')否
timeout请求超时时间(单位为秒),可使用浮点类型或元组类型。若使用浮点类型,则表示总超时时间; 若使用元组类型,则表示连接和读取超时时间,比如: (connect_timeout, read_timeout)。总时间超时、连接超时和读取超时分别会抛出Timeout、ConnectTimeout和ReadTimeout异常否
allow_redirects是否允许重定向,使用布尔类型,默认为True否
proxies代理服务器,使用字典类型,比如{'http': 'http_target_url', 'https': 'https_target_url'}或{'http_src_url': 'http_target_url'}否
stream是否立即下载响应内容,使用布尔类型,默认为False否
verify服务端安全验证,使用布尔类型(是否验证服务器的TLS证书)或字符串类型(CA bundle文件路径),默认为True
否
cert客户端安全验证,使用字符串类型(SSL客户端证书.pem文件的路径)或元组类型。元组类型如('/path/to/filename.cert', '/path/to/filename.key')否
hooks设置请求的Hook回调函数,详见5.2节否
查看request()函数的源代码后发现,它的实现很简单,除去注释内容后,就只有3行代码,代码如下:
def request(method, url, **kwargs):
with sessions.Session() as session:
return session.request(method=method, url=url, **kwargs)
从以上代码可以看出,request()函数实际上是将发送请求的动作交给了Session对象的request()方法。关于Session详见3.3.5节。
3.3.5使用会话
如果直接使用get()、post()等函数发送请求,每个请求都需要建立和断开会话,这无形之中增加了系统开销。更好的方式是在多个请求中共用会话,在Requests中的会话用Session对象来表示。
仍然以登录IMS为例,看看在不使用会话的情况下请求的调用耗时。为此新增use_session模块,代码如下:
import time
import requests
body = {
'username': 'admin',
'password': 'admin123456'
}
start = time.time()
for i in range(10):
response = requests.post('http://ims.lujiatao.com/api/login', data=body)
cookie = response.cookies
requests.get('http://ims.lujiatao.com/api/goods/all', cookies=cookie)
print(f'耗时:{time.time() - start}')
/goods/all接口用于获取IMS中的全部商品。多次执行以上代码,在笔者的计算机上耗时基本维持在1.3~1.5s。
接着使用会话做同样的操作,观察耗时情况,代码如下:
start = time.time()
for i in range(10):
with Session() as session:
response = session.post('http://ims.lujiatao.com/api/login', data=body)
cookie = response.cookies
session.get('http://ims.lujiatao.com/api/goods/all', cookies=cookie)
print(f'耗时:{time.time() - start}')
以上代码使用了with…as语句将使用会话的代码进行了包裹,以便使用完后可以自动断开会话。
要执行以上代码,还需要增加导入语句,代码如下:
from requests import Session
多次执行以上代码,在笔者的计算机上耗时基本维持在1.2~1.4s。因此使用会话的耗时低于不使用会话的耗时,即使用会话提高了多个请求的执行效率。
除了提高效率,使用会话的另一个重要作用是共用数据,最常见的是共用Cookie。在以上共用会话的代码中,可以省略操作Cookie的部分,修改后with…as语句中的代码如下:
session.post('http://ims.lujiatao.com/api/login', data=body)
session.get('http://ims.lujiatao.com/api/goods/all')
重新执行以上代码,执行结果仍然是成功的,因此使用会话后不再需要显式保存和传递Cookie,即Cookie在会话中被自动共用了。
另一种数据共用是直接在Session对象中设置会话级默认数据,这样会话中的每个请求都默认携带该数据,代码如下:
with Session() as session:
session.headers = {
'key_01': 'value_01'
}
response = session.post('http://ims.lujiatao.com/api/login', data=body)
first = response.request.headers['key_01']
response = session.get('http://ims.lujiatao.com/api/goods/all')
second = response.request.headers['key_01']
assert first == second
对于会话中的具体请求而言,其传入的参数优先级更高,可以覆盖会话级默认数据,为此在以上with...as语句中增加request_headers作为具体请求的请求头来演示这个过程,代码如下:
request_headers = {
'key_01': 'value_01_new',
'key_02': 'value_02',
}
response = session.get('http://ims.lujiatao.com/api/goods/all', headers=request_headers)
print(response.request.headers)
重新执行测试代码,执行结果如下:
{'key_01': 'value_01_new', 'key_02': 'value_02', 'Cookie': 'JSESSIONID=22872B2AA4E7B8A4AA6B6605C09065D2'}
从以上输出可以看出,当会话中的具体请求传入的数据与默认数据相同时,会覆盖默认数据(以上输出中的'key_01': 'value_01_new'); 当不相同时,就追加传入的数据(以上输出中的'key_02': 'value_02')。
3.3.6上传和下载文件
文件的上传和下载是应用程序的常见功能,因此必须掌握如何通过接口来上传和下载文件。
1. 上传文件
最常见的是Multipart类型的文件上传一般通过表单提交,其ContentType为multipart/formdata,示例表单代码如下:
在Requests中,可以使用files参数模拟文件的上传,代码如下:
import requests
requests.post('http://your-ip:your-port/upload', files={'my-file': open('path/to/file', 'rb')})
注意: 建议使用二进制方式打开文件,因为Requests会尝试自动设置ContentLength,其值为文件中的字节数,如果以文本方式打开可能会出错。
以上代码中的yourip、yourport和path/to/file分别表示服务器的IP地址、端口号和待上传文件的全路径,读者应根据实际情况进行替换。myfile对应的是表单中input标签的name属性值。
可以为待上传的文件提供更多信息,如文件名、ContentType和自定义的请求头等,代码如下:
request_headers = {
'key_01': 'value_01',
'key_02': 'value_02',
}
requests.post('http://your-ip:your-port/upload',
files={'my-file': ('my-filename.png', open('path/to/file', 'rb'), 'image/png', request_headers)})
以上代码将文件名指定为myfilename.png,将ContentType指定为image/png,并提供了两个自定义的请求头key_01和key_02。
除了单个文件上传,Requests也支持多个文件上传,代码如下:
files = [
('my-file', open('path/to/file_01', 'rb')),
('my-file', open('path/to/file_02', 'rb')),
]
requests.post('http://your-ip:your-port/upload', files=files)
2. 下载文件
下载文件需要访问Response对象的content属性,以便以二进制形式获取响应体。
以下载httpbin.org的收藏夹图标(favicon.ico)为例,代码如下:
response = requests.get('https://httpbin.org/static/favicon.ico')
with open(r'E:\favicon.ico', 'wb') as file:
file.write(response.content)
执行以上代码后,E盘根目录会生成一个favicon.ico文件。
对于大文件的下载建议使用分段传输,代码如下:
response = requests.get('https://httpbin.org/static/favicon.ico', stream=True)
with open(r'E:\favicon.ico', 'wb') as file:
for chunk in response.iter_content(chunk_size=128):
file.write(chunk)
使用分段传输时,需将请求的stream参数指定为True,并使用iter_content()方法来迭代Response对象的二进制内容。另外还可以使用iter_content()方法的chunk_size参数来设置分段大小,默认的分段大小为1。
以上代码虽然实现了分段传输,但是有一个问题: 延迟下载响应内容(即stream参数指定为True)后,Requests并不会自动释放连接。为了解决这个问题,可以手动调用Response对象的close()方法,但更好的方式是使用with...as语句自动释放连接,代码如下:
with requests.get('https://httpbin.org/static/favicon.ico', stream=True) as response:
with open(r'E:\favicon.ico', 'wb') as file:
for chunk in response.iter_content(chunk_size=128):
file.write(chunk)
3.4测试Dubbo接口
在开始测试Dubbo接口之前,请先搭建好IMS Dubbo环境,详见电子版附录A.4节。
说明IMS(Inventory Management System,库存管理系统)是笔者开发的一个用于学习自动化测试的项目,分为Spring Cloud和Dubbo两个版本,前者已经部署到公网(网址详见前言二维码),而后者需要读者自己部署。
有多种方式可以对Dubbo接口进行测试,包括使用Java API、Spring XML、Spring注解、Spring Boot、泛化调用和Python客户端等。那么这些方式应该如何选择呢?
(1) Dubbo接口对测试人员公开: 测试人员能获得Dubbo接口相关依赖时,推荐使用Java API或Spring(Spring XML、Spring注解或Spring Boot)来测试Dubbo接口。
(2) Dubbo接口对测试人员隐藏: 测试人员不能获得Dubbo接口相关依赖时,推荐使用泛化调用来测试Dubbo接口。
(3) 使用其他语言来测试Dubbo接口: 比如若使用Python来测试Dubbo接口,则需要使用Python客户端。
3.4.1使用Java API
由于本节需要使用到Java,因此笔者首先创建了一个新的Maven工程masteringtestautomationfordubbo。然后修改工程的pom.xml文件引入相关依赖,依赖如下:
org.apache.dubbo
dubbo
2.7.11
org.apache.dubbo
dubbo-configcenter-zookeeper
2.7.11
com.lujiatao
ims-api
1.0.0
以上配置中,dubbo是Dubbo框架的基础依赖,dubboconfigcenterzookeeper是Dubbo Zookeeper注册中心的依赖,而imsapi是IMS的Dubbo接口。
待依赖下载完成后,读者可以先熟悉一下IMS的Dubbo接口源代码。其中,只有GoodsCategoryService(物品类别)、GoodsService(物品)和UserService(用户)3个简单的接口,如图34所示。
图34IMS的Dubbo接口
接着笔者以测试GoodsService接口的getById()方法为例介绍Dubbo接口的测试,该方法用于根据物品ID获取物品。为此先在/src/test/java目录创建com.lujiatao.dubbo包,并新增JavaAPI类。
说明根据Maven工程的规范,测试代码应该存放在/src/test/java目录,非测试代码应该存放在/src/main/java目录。
使用Java API测试Dubbo接口可以看作是模拟一个服务消费者来调用服务提供者提供的Dubbo接口,而服务消费者本质上属于一个应用程序,因此首先需要配置该应用程序。在Dubbo中,使用ApplicationConfig类来配置应用程序的代码如下:
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("JavaAPI");
以上代码将应用程序的名称配置为JavaAPI。
然后需要配置注册中心。为了防止超时,笔者将超时时间设置为10s(10000ms),代码如下:
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setTimeout(10000);
registryConfig.setAddress("zookeeper://192.168.3.102:10003");
由于IMS使用ZooKeeper作为注册中心,因此以上代码使用了zookeeper作为注册中心地址的前缀,而192.168.3.102和10003是笔者搭建的注册中心IP地址和端口,读者应根据实际情况进行替换。
接着创建Dubbo接口引用的环节,需要使用到ReferenceConfig类,代码如下:
ReferenceConfig referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setRegistry(registryConfig);
referenceConfig.setInterface(GoodsService.class);
referenceConfig.setVersion("1.0.0");
ReferenceConfig是一个泛型类,由于这里要测试GoodsService接口,因此将GoodsService作为类型形参传递给ReferenceConfig。另外需要使用之前创建的应用程序和注册中心配置,并且还需要设置接口及其版本号。
最后调用ReferenceConfig对象的get()方法获取到目标接口GoodsService,然后调用其中的getById()方法,代码如下:
GoodsService goodsService = referenceConfig.get();
Goods goods = goodsService.getById(1);
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(goods));
以上代码使用到了Gson,其用于将对象序列化为JSON。
运行JavaAPI类,执行结果如下:
{"id":1,"brand":"HUAWEI","model":"Mate 40","desc":"","count":0,"goodsCategoryId":1,"createTime":"2021-05-19 08:51:14","updateTime":"2021-05-19 08:51:14"}
由于IMS每次启动时会重新初始化数据库,因此读者看到的createTime和updateTime会与上述结果不一致。
有时出于测试目的,希望绕开注册中心直接调用Dubbo接口,这种方式被称为点对点直连。点对点直接需要删除配置注册中心的代码,并调用ReferenceConfig对象的setUrl()方法设置直连URL即可。
【例34】Dubbo接口的点对点直接。
package com.lujiatao.dubbo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lujiatao.ims.api.GoodsService;
import com.lujiatao.ims.common.entity.Goods;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
public class JavaAPI {
public static void main(String[] args) {
// 配置应用程序
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("JavaAPI");
// 创建Dubbo接口引用
ReferenceConfig referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setUrl("dubbo://192.168.3.102:10001");
referenceConfig.setInterface(GoodsService.class);
referenceConfig.setVersion("1.0.0");
// 调用Dubbo接口
GoodsService goodsService = referenceConfig.get();
Goods goods = goodsService.getById(1);
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(goods));
}
}
不管是使用注册中心还是点对点直连方式,当调用了Dubbo接口后,就可以使用单元测试框架编写自动化测试用例了。Java中的单元测试框架有TestNG、JUnit等。
3.4.2使用Spring XML
使用XML是Spring的传统配置方式,因此也可以使用这种方法来配置一个调用Dubbo接口的测试应用程序。
首先创建XML配置文件,为此在/src/test目录中新增resources目录,在resources目录中新增consumer.xml配置文件,文件内容如下:
以上配置可以看成3.4.1节中的XML配置版本,其中也配置了应用程序名称、注册中心超时时间和地址、接口及其版本号。另外,配置中的id用于在代码中引用该接口。
以上配置完成后,就可以在com.lujiatao.dubbo包中新增SpringXML类用于Dubbo接口测试了。
【例35】使用Spring XML进行Dubbo接口测试。
package com.lujiatao.dubbo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lujiatao.ims.api.GoodsService;
import com.lujiatao.ims.common.entity.Goods;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringXML {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("consumer.xml");
context.start();
GoodsService goodsService = (GoodsService) context.getBean("goodsService");
Goods goods = goodsService.getById(1);
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(goods));
context.close();
}
}
以上代码使用了ClassPathXmlApplicationContext来加载XML配置,它是Spring中的一种应用程序上下文。getBean()方法用于获取一个Bean(以上示例的GoodsService),其参数goodsService对应XML配置中的id。另外在使用应用程序上下文时,需使用start()和close()方法来分别执行启动和关闭操作。
XML配置同样支持点对点直连,为此删除注册中心的相关配置。内容如下:
接着增加直连URL,内容如下:
3.4.3使用Spring注解
对于XML中的部分配置可以使用Properties来替代,Dubbo会默认加载dubbo.properties配置文件。可被替代的XML配置如下:
为了替代以上XML配置,在/src/test/resources目录中新增dubbo.properties配置文件,文件内容如下:
dubbo.application.name=SpringXML
dubbo.registry.timeout=10000
dubbo.registry.address=zookeeper://192.168.3.102:10003
在将应用程序和注册中心的配置换成使用Properties配置文件后,还可以直接使用注解在Java类中替换XML配置中的Dubbo接口引用部分。为此新增Consumer类,代码如下:
package com.lujiatao.dubbo;
import com.lujiatao.ims.api.GoodsService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableDubbo
public class Consumer {
@DubboReference(version = "1.0.0")
private GoodsService goodsService;
@Bean("goodsService")
public GoodsService getGoodsService() {
return goodsService;
}
}
以上代码中的@Configuration注解用于修饰一个Spring配置类,而@EnableDubbo注解用于使Dubbo组件成为一个Spring Bean。
说明如果Properties配置文件名称不是dubbo.properties,就需要显式引入该配置文件,比如配置文件名为mydubbo.properties,则需要使用@PropertySource("/mydubbo.properties")注解来修饰Consumer类。
接着新增SpringAnnotation类用于Dubbo接口的调用。
【例36】使用Spring注解进行Dubbo接口测试。
package com.lujiatao.dubbo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lujiatao.ims.api.GoodsService;
import com.lujiatao.ims.common.entity.Goods;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SpringAnnotation {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Consumer.class);
context.start();
GoodsService goodsService = (GoodsService) context.getBean("goodsService");
Goods goods = goodsService.getById(1);
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(goods));
context.close();
}
}
以上代码跟3.4.2节中的SpringXML类很相似,唯一不同的是应用程序上下文的初始化方式,这里使用的是AnnotationConfigApplicationContext类。
若需使用点对点直连,则需要修改Consumer类中的@DubboReference注解,新增url参数,代码如下:
@DubboReference(url = "dubbo://192.168.3.102:10001", version = "1.0.0")
然后删除dubbo.properties文件中的注册中心相关配置即可。
3.4.4使用Spring Boot
Spring Boot可以看成对Spring的一个开箱即用的封装,它同时也是构建Spring微服务的基础,因此使用它来调用Dubbo接口是目前最常见的方式。
要使用Spring Boot调用Dubbo接口,首先需要在pom.xml文件中增加相关的依赖,依赖如下:
org.apache.dubbo
dubbo-spring-boot-starter
2.7.11
org.springframework.boot
spring-boot-starter
2.3.8.RELEASE
ch.qos.logback
logback-classic
dubbospringbootstarter是Dubbo的Spring Boot配置依赖,而springbootstarter是Spring Boot的基础依赖。以上配置排除了logbackclassic依赖,否则在Spring Boot启动时会抛出IllegalArgumentException异常,并提示LoggerFactory is not a Logback LoggerContext but Logback is on the classpath.。
另外,由于springbootstarter与dubbo的snakeyaml依赖版本有冲突,需要把dubbo中的snakeyaml依赖排除掉。masteringtestautomationfordubbo工程的完整依赖如下:
org.apache.dubbo
dubbo
2.7.11
org.yaml
snakeyaml
org.apache.dubbo
dubbo-configcenter-zookeeper
2.7.11
com.lujiatao
ims-api
1.0.0
org.apache.dubbo
dubbo-spring-boot-starter
2.7.11
org.springframework.boot
spring-boot-starter
2.3.8.RELEASE
ch.qos.logback
logback-classic
接着在com.lujiatao.dubbo包中新增SpringBoot类用于Dubbo接口测试。
【例37】使用Spring Boot进行Dubbo接口测试。
package com.lujiatao.dubbo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lujiatao.ims.api.GoodsService;
import com.lujiatao.ims.common.entity.Goods;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SpringBoot {
@DubboReference(version = "1.0.0")
private GoodsService goodsService;
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBoot.class, args);
GoodsService goodsService = (GoodsService) context.getBean("goodsService");
Goods goods = goodsService.getById(1);
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(goods));
}
@Bean("goodsService")
public GoodsService getGoodsService() {
return goodsService;
}
}
以上代码可以看成3.4.3节中的SpringAnnotation和Consumer类的合并版本。不同之处在于应用程序上下文的初始化方式,这里使用的是ConfigurableApplicationContext类。
仅仅有以上代码还无法启动该Spring Boot应用程序,还需要增加配置信息。在Spring Boot中,通常使用YAML文件来配置应用程序。为此在/src/test/resources目录新增application.yml文件,文件内容如下:
dubbo:
application:
name: SpringBoot
registry:
timeout: 10000
address: zookeeper://192.168.3.102:10003
server:
port: 8081
8081是笔者使用的Spring Boot应用程序端口号,读者可根据实际情况修改端口号。
做完以上配置后,运行Spring Boot类便可以调用Dubbo接口了。
若需使用点对点直连,需要修改Spring Boot类中的@DubboReference注解,新增url参数,代码如下:
@DubboReference(url = "dubbo://192.168.3.102:10001", version = "1.0.0")
然后删除application.yml文件中的注册中心相关配置即可。
3.4.5使用泛化调用
泛化调用可以使用Java API、Spring(Spring XML、Spring注解或Spring Boot)或框架/工具来调用Dubbo接口。本节介绍使用Java API和JMeter两种方式来调用Dubbo接口。
1. 使用Java API调用Dubbo接口
使用Java API通过泛化调用方式来调用Dubbo接口需要用到GenericService接口。服务消费者(客户端)使用GenericService实现的是泛化调用,而服务提供者(服务端)使用GenericService实现的是泛化实现(详见3.5.2节)。
在com.lujiatao.dubbo包中新增GenericInvoke类用于测试Dubbo接口。
【例38】使用泛化调用进行Dubbo接口测试。
package com.lujiatao.dubbo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.rpc.service.GenericService;
public class GenericInvoke {
public static void main(String[] args) {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("GenericInvoke");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setTimeout(10000);
registryConfig.setAddress("zookeeper://192.168.3.102:10003");
ReferenceConfig referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setRegistry(registryConfig);
referenceConfig.setInterface("com.lujiatao.ims.api.GoodsService");
referenceConfig.setVersion("1.0.0");
referenceConfig.setGeneric(true);
GenericService genericService = referenceConfig.get();
Object goods = genericService.$invoke("getById", new String[]{"int"}, new Object[]{1});
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(goods));
}
}
以上代码整体跟3.4.1节中的例34相似度很高,笔者重点介绍下它们之间的不同点。首先ReferenceConfig的类型形参被替换成了GenericService,GoodsService.class也被替换成了字符串com.lujiatao.ims.api.GoodsService,因为泛化调用是不依赖具体的Dubbo接口的。然后需要调用ReferenceConfig对象的setGeneric()方法,并传入true以告诉Dubbo此时需要使用泛化调用。泛化调用的核心是$invoke()方法,它支持3个参数,第一个参数(“getById”)表示方法名,第二个参数(“int”)表示方法的参数类型,第三个参数(1)表示方法的参数值。由于参数可能有多个,因此使用了字符串数组来传递参数的类型,使用对象数组来传递参数的值。
运行GenericInvoke类,执行结果如下:
{"goodsCategoryId":1,"createTime":"2021-05-19 08:51:14","count":0,"model":"Mate 40","updateTime":"2021-05-19 08:51:14","id":1,"class":"com.lujiatao.ims.common.entity.Goods","brand":"HUAWEI","desc":""}
从以上输出可以看出,使用泛化调用的返回结果会多一个class,其值表示该返回对象的类型。以上示例返回了一个Goods对象。
若需使用点对点直连,直接参考3.4.1节即可。
使用泛化调用时,需要注意传参方式。如果是Java的8种基本类型,直接传递即可; 如果不是基本类型,就需要使用参数类型的全名,比如String类型必须写成java.lang.String。针对参数为POJO(Plain Old Java Object,简单的Java对象)的情况,参数值需要使用Map类型进行传递。以在IMS中新增一个物品为例,传参的写法如下:
Map params = new HashMap<>();
params.put("goodsCategoryId", 1);
params.put("model", "New Model");
params.put("count", 0);
params.put("brand", "New Brand");
params.put("desc", "");
genericService.$invoke("add", new String[]{"com.lujiatao.ims.common.entity.Goods"}, new Object[]{params});
以上createTime、updateTime和id 3个字段都没有传递,因为IMS的数据库表会自动生成这些字段的默认值。
2. 使用JMeter调用Dubbo接口
Dubbo官方推荐使用Apache JMeter Plugin For Apache Dubbo插件为JMeter提供Dubbo接口的调用能力。
以Windows操作系统为例,首先访问Apache JMeter Plugin For Apache Dubbo的下载地址,地址详见前言二维码。
截至笔者写作本书时,最新的插件版本为2.7.8,因此笔者下载的插件文件名为jmeterpluginsdubbo2.7.8jarwithdependencies.jar,下载后将该文件复制到JMeter的/lib/ext目录,接着使用插件提供的Dubbo Sample进行Dubbo接口调用即可。参数填写如图35所示。
图35Dubbo Sample的参数填写
执行结果与使用Java API进行泛化调用的执行结果是一致的。
若需使用点对点直连,则将Register Center的Protocol改为none,并将Address改为192.168.3.102:10003即可。
相比使用Java API,使用Apache JMeter Plugin For Apache Dubbo来传递POJO类型的参数就简单得多了。仍然以在IMS中新增一个物品为例,直接将paramType和paramValue分别修改为com.lujiatao.ims.common.entity.Goods和{"goodsCategoryId":1,"count":0,"model":"New Model","brand":"New Brand","desc":""}即可。
3.4.6使用Python客户端
由于Dubbo官方推荐的Python客户端Python Client For Apache Dubbo并不支持Python 3且需要服务提供者使用JSONRPC,因此笔者使用了另一个Python客户端pythondubbosupportpython 3来替代。
执行命令即可安装pythondubbosupportpython3,命令如下:
pip install python-dubbo-support-python3
本节继续使用masteringtestautomation工程,并在chapter_03包中新增learning_dubbo_test子包,在learning_dubbo_test子包中新增learning_dubbo_test模块,并编写Dubbo接口测试代码。
【例39】使用Python客户端进行Dubbo接口测试。
from dubbo.client import ZkRegister, DubboClient
zookeeper = ZkRegister('192.168.3.102:10003')
dubbo_client = DubboClient('com.lujiatao.ims.api.GoodsService', zk_register=zookeeper)
result = dubbo_client.call('getById', 1)
print(result)
ZkRegister和DubboClient类分别表示注册中心和Dubbo客户端。DubboClient对象的call()方法用于调用Dubbo接口,其第一个参数是方法名,第二个参数是元组类型,用于表示方法的参数,第三个参数表示超时时间(单位为秒)。
执行以上代码,执行结果如下:
{'updateTime': '2021-05-19 08:51:14', 'createTime': '2021-05-19 08:51:14', 'goodsCategoryId': 1, 'count': 0, 'desc': '', 'model': 'Mate 40', 'brand': 'HUAWEI', 'id': 1}
从以上输出可以看出,使用Python客户端调用Dubbo接口的返回值与使用Java是一致的。
若需使用点对点直连,则删除掉注册中心相关配置,并在创建DubboClient对象时提供直连URL即可,代码如下:
dubbo_client = DubboClient('com.lujiatao.ims.api.GoodsService', host='192.168.3.102:10001')
pythondubbosupportpython3同样支持POJO类型的参数传递。仍以在IMS中新增一个物品为例,代码如下:
goods = Object('com.lujiatao.ims.common.entity.Goods')
goods['goodsCategoryId'] = 1
goods['count'] = 0
goods['model'] = 'New Model'
goods['brand'] = 'New Brand'
goods['desc'] = ''
dubbo_client.call('add', goods)
以上代码中的Object类用于表示一个Java对象,其需要单独导入,代码如下:
from dubbo.codec.encoder import Object
3.5Mock测试
3.5.1HTTP接口测试的Mock
用于HTTP接口测试的Mock框架/工具有很多,比如Responses、WireMock等,还有像RAP这类接口管理工具,其提供了包括Mock在内的多种功能。本节以Responses为例介绍Mock是如何进行HTTP接口测试的。
Responses用于模拟Requests的响应数据,因此可以使用Responses模拟HTTP接口的返回值。
要使用Responses,需要执行命令安装它,命令如下:
pip install responses
以登录IMS为例,在不使用Mock的情况下,编写一个正常的测试函数,代码如下:
import requests
body = {
'username': 'admin',
'password': 'admin123456'
}
def test_normal():
response = requests.post('http://ims.lujiatao.com/api/login', data=body)
assert response.json() == {'code': 0, 'msg': '', 'data': None}
使用Responses可以轻松模拟IMS的返回数据,为此新增一个测试函数test_mock_01(),代码如下:
@responses.activate
def test_mock_01():
responses.add(responses.POST, 'http://ims.lujiatao.com/api/login', json={'code': 0, 'msg': '', 'data': None})
response = requests.post('http://ims.lujiatao.com/api/login', data=body)
assert response.json() == {'code': 0, 'msg': '', 'data': None}
以上代码首先使用@responses.activate装饰器激活Responses的Mock功能,然后添加需要Mock的请求,包括请求方法、URL和返回数据。
当然在执行以上测试代码之前还需要导入responses,代码如下:
import responses
现在的问题是如何证明发起的接口请求命中的是Mock接口,而非真正的IMS接口呢?Responses提供了一个CallList对象用于存放调用信息,可使用responses.calls对其进行快捷访问,代码如下:
@responses.activate
def test_mock_01():
assert len(responses.calls) == 0 # 新增
responses.add(responses.POST, 'http://ims.lujiatao.com/api/login', json={'code': 0, 'msg': '', 'data': None})
response = requests.post('http://ims.lujiatao.com/api/login', data=body)
assert response.json() == {'code': 0, 'msg': '', 'data': None}
assert len(responses.calls) == 1 # 新增
以上代码在调用接口的前后判断了CallList对象的长度,以此证明发起的接口请求确实命中的是Mock接口。
除了使用@responses.activate装饰器激活Responses的Mock功能,也可以使用with…as语句,代码如下:
def test_mock_02():
with responses.RequestsMock() as response:
response.add(responses.POST, 'http://ims.lujiatao.com/api/login', json={'code': 0, 'msg': '', 'data': None})
response = requests.post('http://ims.lujiatao.com/api/login', data=body)
assert response.json() == {'code': 0, 'msg': '', 'data': None}
这种情况适合测试函数/方法中只有部分请求需要Mock的场景。
以上只是对Responses的简单介绍,有兴趣的读者可查阅Responses的GitHub文档,地址详见前言二维码。
3.5.2Dubbo接口测试的Mock
由于目前Dubbo接口没有成熟的开源Mock测试框架/工具,因此在本节,笔者将通过实现GenericService接口来实现一个Dubbo的Mock服务器。
首先在masteringtestautomationfordubbo工程中新增一个名为mockserver的Maven模块,在该模块的/src/main/java目录中新增com.lujiatao.mockserver包,在com.lujiatao.mockserver包中新增GenericServiceImpl类,代码如下:
package com.lujiatao.mockserver;
import org.apache.dubbo.rpc.service.GenericException;
import org.apache.dubbo.rpc.service.GenericService;
import java.util.HashMap;
import java.util.Map;
public class GenericServiceImpl implements GenericService {
@Override
public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
if (method.equals("getById")) {
if (parameterTypes.length == 1) {
if (parameterTypes[0].equals("int")) {
Map params = new HashMap<>();
params.put("id", 1);
params.put("brand", "HUAWEI");
params.put("model", "Mate 40");
params.put("desc", "");
params.put("count", 0);
params.put("goodsCategoryId", 1);
params.put("createTime", "2021-05-19 08:51:14");
params.put("updateTime", "2021-05-19 08:51:14");
params.put("class", "com.lujiatao.ims.common.entity.Goods");
return params;
}
}
}
throw new IllegalArgumentException("不存在该Mock方法");
}
}
以上代码重写了GenericService接口的$invoke()方法,在方法体中判断了泛化调用时的方法名、参数数量及类型,最后使用Map代替POJO(以上示例中的Goods)返回给方法调用者。
新增MockServer类,代码如下:
package com.lujiatao.mockserver;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.ServiceConfig;
import org.apache.dubbo.rpc.service.GenericService;
import java.util.concurrent.CountDownLatch;
public class MockServer {
public static void main(String[] args) throws InterruptedException {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("MockServer");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setTimeout(10000);
registryConfig.setAddress("zookeeper://192.168.3.102:10003");
ServiceConfig serviceConfig = new ServiceConfig<>();
serviceConfig.setApplication(applicationConfig);
serviceConfig.setRegistry(registryConfig);
serviceConfig.setInterface("com.lujiatao.ims.api.GoodsService");
serviceConfig.setVersion("1.0.0");
serviceConfig.setRef(new GenericServiceImpl());
serviceConfig.export();
new CountDownLatch(1).await();
}
}
ServiceConfig类与ReferenceConfig类功能相对应,前者用于暴露接口,后者用于引用接口。以上代码创建了一个ServiceConfig对象,并将对GoodsService接口的调用映射到GenericServiceImpl类,即将对真实接口的调用映射到GenericService接口的实现类。配置好映射关系后,需要使用export()方法暴露该服务提供者。另外,在最后创建CountDownLatch对象并调用其await()方法的目的是阻塞主线程,否则在执行MockServer类时,主线程将很快结束,即无法使Mock服务器持续地运行。
最后到了验证Mock服务器的时候了。首先停止已启动的IMS服务提供者,然后启动Mock服务器(即运行MockServer类)。接着便可以使用泛化调用的方式来调用Dubbo接口了。
说明出于演示目的,该Mock服务器仅提供了GoodsService接口getById()方法的Mock,且返回值也仅仅是硬编码的。