第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到AcceptLanguage: zhCN,zh;q=0.9,en;q=0.8之间的所有内容都是请求头,而Server: nginx/1.19.6到AcceptRanges: 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 Networkbased 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个完整示例如表31所示。 表31REST接口的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) JSONRPC(JavaScript Object NotationRemote Method Invocation,JavaScript对象表示法远程方法调用)。JSONRPC是一种无状态、轻量级的RPC实现,它使用JSON作为数据格式。 (4) XMLRPC(Extensible Markup LanguageRemote Method Invocation,可扩展标记语言远程方法调用)。 XMLRPC是使用XML作为数据格式、HTTP作为传输机制的RPC实现。 (5) Apache Dubbo(以下简称为Dubbo)。其是一个高性能、轻量级的开源Java微服务框架。 2. Dubbo和Dubbo接口 Dubbo是一个高性能、轻量级的开源Java微服务框架,它是一个RPC的实现框架。Dubbo提供了以下六大核心能力。 (1) 面向接口代理的高性能RPC调用: 基于代理的高性能远程调用能力,服务以接口为粒度,为开发人员屏蔽了远程调用的底层细节。 (2) 智能负载均衡: 内置了多种负载均衡策略,可智能感知下游节点的健康状况,从而显著减少调用延迟,以提高系统的吞吐量。 (3) 服务自动注册及发现: 支持多种类型的注册中心,可实时感知服务实例的上下线。 (4) 高度可扩展能力: 遵循“微内核+插件”的设计原则,所有核心能力如协议、网络传输及序列化等被设计为扩展点,平等对待内置实现和第三方实现。 (5) 运行期间流量调度: 内置条件、脚本等路由策略,通过配置不同的路由策略,可轻松实现灰度发布、同机房优先等功能。 (6) 可视化的服务治理与运维: 提供丰富的服务治理和运维工具——随时查询服务元数据、服务健康状态及调用统计,并可以实时下发路由策略及调整配置参数。 Dubbo的基本架构如图31所示。 图31Dubbo的基本架构 注: 虚线表示初始化; 实线表示同步调用; 点线表示异步调用 首先,由容器启动服务提供者,并将服务提供者注册到注册中心,即向注册中心声明可以提供的服务。然后,服务消费者向注册中心请求需要使用的服务,注册中心将可用的服务返回给服务消费者。由于当可用的服务发生变化时,注册中心需要通知服务消费者,因此服务消费者对于注册中心来说是一个订阅者的角色,而注册中心对于服务消费者来说是一个发布者的角色。最后,服务消费者调用服务提供者提供的服务完成应用程序的请求。对于监控中心而言,需要同时统计服务消费者和服务提供者的调用信息(包括调用次数和调用时间)。另外,注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。 以上都是对Dubbo框架的介绍,那么Dubbo接口又是什么呢?在Dubbo框架中,服务提供者将接口暴露为可远程调用的服务,因此Dubbo接口本质上就是Java接口,只是这些接口需要通过Dubbo框架暴露给服务消费者以供调用。 3.2查看接口的辅助工具 3.2.1浏览器开发者工具 使用开发者工具可以方便地看到当前浏览器中的HTTP请求和响应数据,提供开发者工具的主要浏览器包括Chrome、Edge、Safari和Firefox等。 以Chrome为例,在Windows计算机中使用F12键或Ctrl+Shift+I组合键打开开发者工具在Network标签页可看到HTTP请求和响应数据,如图32所示。 图32使用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中查看到数据,如图33所示。 图33使用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为keepalive。 (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模块,然后编写一些简单的请求。 【例31】编写一些简单的请求。 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) 以上代码首先把响应内容设置为UTF8编码,然后再获取响应体。 3.3.2构建请求参数 1. 构建URL参数 URL参数常用于GET请求的参数携带。 新增build_request_param模块,编写构建URL参数的代码。 【例32】编写构建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中,并返回给调用者。 说明请求头的名称对字母的大小写不敏感,如UserAgent和useragent表示的是一样的。 以上示例中的返回值都是经过httpbin.org特殊处理的,实际项目的返回值肯定是不同的,这点需要读者特别注意。另外,对于文件上传的参数构建会有所区别,详见3.3.6节。 3.3.3操作Cookie 客户端(比如浏览器)在与服务器的交互过程中,为了验证客户端的合法性,当客户端与服务器建立连接并认证通过后,服务器会将会话ID放在响应头的SetCookie中,客户端在获取会话ID后,在后续请求中将会话ID通过Cookie的形式携带并发送回服务器。 在Requests中,访问Response对象的cookies属性可以获取Cookie,而Cookie由一个RequestsCookieJar对象表示。新增operate_cookie模块,并以登录IMS为例演示Cookie的获取。 【例33】获取登录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外,其他都是非必填参数。具体详见表32。 表32request()函数参数 参 数 名 称参 数 含 义必填 methodHTTP请求方法,支持GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS和TRACE。对大小写不敏感,即传入get、Get或GET都表示GET请求是 url请求的URL是 paramsURL查询参数,通常使用字典类型,也可使用列表(列表元素为元组)等否 data请求体,通常使用字典类型,也可使用列表(列表元素为元组)等。ContentType默认为application/xwwwformurlencoded否续表 参 数 名 称参 数 含 义必填 json请求体,通常使用字典类型,也可使用可序列化的Python对象。ContentType默认为application/json 否 headers请求头,使用字典类型否 cookiesCookie,使用字典类型或CookieJar类型对象。当然也可以使用RequestsCookieJar类型对象,因为RequestsCookieJar是CookieJar的子类否 files待上传的文件,ContentType为multipart/formdata。详见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类型的文件上传一般通过表单提交,其ContentType为multipart/formdata,示例表单代码如下:
在Requests中,可以使用files参数模拟文件的上传,代码如下: import requests requests.post('http://your-ip:your-port/upload', files={'my-file': open('path/to/file', 'rb')}) 注意: 建议使用二进制方式打开文件,因为Requests会尝试自动设置ContentLength,其值为文件中的字节数,如果以文本方式打开可能会出错。 以上代码中的yourip、yourport和path/to/file分别表示服务器的IP地址、端口号和待上传文件的全路径,读者应根据实际情况进行替换。myfile对应的是表单中input标签的name属性值。 可以为待上传的文件提供更多信息,如文件名、ContentType和自定义的请求头等,代码如下: 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)}) 以上代码将文件名指定为myfilename.png,将ContentType指定为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工程masteringtestautomationfordubbo。然后修改工程的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框架的基础依赖,dubboconfigcenterzookeeper是Dubbo Zookeeper注册中心的依赖,而imsapi是IMS的Dubbo接口。 待依赖下载完成后,读者可以先熟悉一下IMS的Dubbo接口源代码。其中,只有GoodsCategoryService(物品类别)、GoodsService(物品)和UserService(用户)3个简单的接口,如图34所示。 图34IMS的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即可。 【例34】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接口测试了。 【例35】使用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,就需要显式引入该配置文件,比如配置文件名为mydubbo.properties,则需要使用@PropertySource("/mydubbo.properties")注解来修饰Consumer类。 接着新增SpringAnnotation类用于Dubbo接口的调用。 【例36】使用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 dubbospringbootstarter是Dubbo的Spring Boot配置依赖,而springbootstarter是Spring Boot的基础依赖。以上配置排除了logbackclassic依赖,否则在Spring Boot启动时会抛出IllegalArgumentException异常,并提示LoggerFactory is not a Logback LoggerContext but Logback is on the classpath.。 另外,由于springbootstarter与dubbo的snakeyaml依赖版本有冲突,需要把dubbo中的snakeyaml依赖排除掉。masteringtestautomationfordubbo工程的完整依赖如下: 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接口测试。 【例37】使用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接口。 【例38】使用泛化调用进行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节中的例34相似度很高,笔者重点介绍下它们之间的不同点。首先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,因此笔者下载的插件文件名为jmeterpluginsdubbo2.7.8jarwithdependencies.jar,下载后将该文件复制到JMeter的/lib/ext目录,接着使用插件提供的Dubbo Sample进行Dubbo接口调用即可。参数填写如图35所示。 图35Dubbo 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且需要服务提供者使用JSONRPC,因此笔者使用了另一个Python客户端pythondubbosupportpython 3来替代。 执行命令即可安装pythondubbosupportpython3,命令如下: pip install python-dubbo-support-python3 本节继续使用masteringtestautomation工程,并在chapter_03包中新增learning_dubbo_test子包,在learning_dubbo_test子包中新增learning_dubbo_test模块,并编写Dubbo接口测试代码。 【例39】使用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') pythondubbosupportpython3同样支持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服务器。 首先在masteringtestautomationfordubbo工程中新增一个名为mockserver的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,且返回值也仅仅是硬编码的。