测试技能提升HM-接口测试
接口测试理论
概念:
接口:系统之间(外部系统与内部系统。内部系统与内部系统)数据交换的通道
接口校验:校验接口回发的响应数据与预期结果是否一致
接口测试:绕过前端界面,直接对服务器进行测试
价值:
可以发现⻚⾯测试发现不了的问题
符合 质量控制前移理念
低成本,⾼收益!
实现方式:
⼯具:
postman:使⽤简单,上⼿难度低。功能较少。
jmeter:使⽤难度较⼤。上⼿难度⼤。功能⻬全。
代码:
Python + requests + Unittest
java + HttpClient
http协议
协议:
就是规则,要求使用协议的双方必须严格遵守
http协议
超文本传输协议,基于请求和响应的应用层协议
特点:
1、客户端、服务器模式
2、简单快速
3、灵活
4、无连接
5、无状态
URL格式
URL:统一资源定位符,网络资源地址,http通过URL来建立连接和传输数据
URL组成:
协议 : // hostname[:port] / path / [? 查询参数1 & 查询参数2]
协议:
http、https作用:指定数据传输规则
IP地址:
域名,在网络环境中唯一定位一台主机
端口号:
在主机上,唯一定义某个应用程序
可以省略,如果省略,跟随协议,http-80,https - 443
资源路径:
应用对应的数据资源
可以省略,如果省略了,资源路径为/
查询参数:
给资源传递参数
可以省略,如果省略,没有?分割符
可以有多组,每组k = v格式,各组之间&隔分
练习:
http://tpshop-test.itheima.net/index.php?m=Home&c=User&a=do_login
协议:http
端口:80
域名:tpshop-test.itheima.net
资源路径:index.php
查询参数:m=Home&c=User&a=do_login
http请求
产生端:一定产生于客户端,当客户端给服务器发送请求时,使用该协议(请求报文,请求包)
整体格式
请求行:请求方法、url、协议版本
请求头:k:v
空行:代表请求头结束
请求体:发送给服务器请求时,携带的数据
POST http://demo.zentao.net/user-login.html HTTP/1.1
Host: demo.zentao.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101
Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://demo.zentao.net/user-login.html
Content-Type: application/x-www-form-urlencoded
Content-Length: 54
Connection: keep-alive
Upgrade-Insecure-Requests: 1
account=demo&password=efc4a3b32e48054865e5a8321cfda3e4
请求行
位置:⼀定位于 http请求协议的,第⼀⾏
格式:请求⽅法(空格) URL(空格) 协议版本
作用:说明请求方法、访问的资源、协议版本
POST http://demo.zentao.net/user-login.html HTTP/1.1
常见的请求方法:
GET:查询–没有请求体(从服务器获取资源)
POST :添加(注册、登录)-从服务器新建一个资源
PUT:修改-从服务器更新资源
DELETE:从服务器删除资源
协议版本:常见 HTTP/1.1
请求头
位于请求⾏之下,空⾏之上的部分。 数据组织格式 ⼀定是 k:v 对
作用:通知服务器客户端请求信息
Content-Type:作用,指定请求体的数据类型
text/html: HTML格式
text/plain:纯文本格式
image/jpeg:jpg图片格式
application/json: JSON数据格式
application/x-www-form-urlencoded: 表单默认的提交数据格式
multipart/form-data: 在表单中进行文件上传时使用
请求体
位置:位于空行之下
作用:传输数据实体
有的请求是没有请求体的,如:get、delete
请求体常在post、put方法中使用
请求体的数据类型, 受 请求头中 Content-Type 的值影响
练习:
以下是抓取的某个HTTP协议数据包的请求报⽂,请问其中,请求⾏,请求头,请求体分别是什么?
POST ://tpshop-test.itheima.net/index.php HTTP/1.1 # 请求行
Host: tpshop-test.itheima.net # 请求头
Connection: keep-alive
Content-Length: 53
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://tpshop-test.itheima.net
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90
Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://tpshop-test.itheima.net/Home/user/login.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: is_mobile=0; province_id=1; city_id=2; district_id=3;
is_distribut=1; PHPSESSID=1b80tii77mhle0fqd2bg52ucg5; cn=0
username=13800138006&password=123456&verify_code=8888
post请求无请求体
练习2
抓包获取 ihrm系统的登录接⼝ 和 tpshop登录接⼝,解析请求数据
http响应
产生端:一定产生于服务端,当服务器收到http请求后,才会产生http响应协议(响应报文、响应包)
整体格式:
响应⾏:协议版本、状态码、状态码描述
响应头:K:V 格式数据。
空⾏:代表响应头 结束。
响应体:服务回发给客户端的 数据。⼏乎 所有的 响应包,都有响应体。
HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
<html>
<head></head>
<body>...</body>
</html>
响应行
协议版本、状态码、状态码描述
位置:响应数据的第一行
作用:描述服务器处理结果
内容:状态行由协议版本号、状态码、状态消息组成
HTTP/1.1 200 OK
状态码由三位数字组成,第一个数字定义响应类别:
1xx:指示信息
2xx:成功
3xx:重定向
4xx:客户端错误
5xx:服务器端错误
常见的响应状态码:
响应头
位于响应行之下,空行之上的部分。数据组织格式是k:v对
作用:描述客户端要是用的一些附加信息
响应体
位置:响应数据空白行之后
作用:服务器返回的数据实体
特点:有图片、json、xml、html等多种类型
响应体中 包含的数据,是接⼝测试过程中,所要使⽤的 实际结果!!
练习:
以下是HTTP协议的响应报⽂内容,请问,状态⾏、响应头、响应体分别是哪⼏个部分?
HTTP/1.1 200 OK # 状态行
Server: nginx # 响应头
Date: Mon, 29 Jun 2020 03:36:28 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Set-Cookie: is_mobile=0; expires=Mon, 29-Jun-2020 04:36:28 GMT;
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Content-Length: 805
{"status":1,"msg":"\u767b\u9646\u6210\u529f","result": # 响应体
{"user_id":3338,"email":"","password":"519475228fe35ad067744465
c42a19b2","paypwd":null,"sex":0,"birthday":0,"user_money":"0.00
","frozen_money":"0.00","distribut_money":"0.00","underling_num
ber":0,"pay_points":100,"address_id":0,"reg_time":1590980161,"l
ast_login":1590980161,"last_ip":"","qq":"","mobile":"13800138006","level_name":"\u6ce8\u518c\u4f1a\u5458"},"url":""}
抓包获取ihrm系统的登录接⼝ 和 tpshop登录接⼝,解析响应数据
接口规范和测试流程
传统风格接口
接⼝统⼀采⽤ get/post 实现 所有操作。
URL 与 资源不是 ⼀⼀对应的。在 URL 中查看出,是何种操作
状态码统⼀返回 200
RESTful风格接口
RESTful是一种网络应用程序的设计风格和开发方式,并不是必须要遵守的标准,只是提供了一组设计原则和约束条件
接⼝使⽤的⽅法,与 http协议的 请求⽅法,⼀⼀对应。
get - 查、post - 增、put - 改、delete - 删
URL 与 资源 ⼀⼀对应!不能从 URL 中,看出 是 何种操作。 需要通过 结合 请求⽅法 来识别何种操作。
响应状态码 使⽤较为全⾯
总结:
传统⻛格接⼝:只⽤ get、post⽅法。 URL 不唯⼀。 统⼀返回 200
RESTful⻛格接⼝:URL 唯⼀,定位资源。结合 请求⽅法对应不同操作。 返回状态码 较灵活
接口测试流程
- 需求分析(产品经理的需求⽂档)
- 接口文档解析(开发编写的 接⼝API⽂档)
- 设计 接⼝测试⽤例(编写 Excel 表格形式的⽤例)
- 准备接⼝测试脚本
postman 工具生成脚本
python 代码 编写脚本 - 执行测试⽤例,跟踪缺陷
- 生成接口测试报告
- 接口自动化化持续集成(可选)
.
接口文档解析
解析接口文档
核心目标
1、请求报文关键数据
请求方法、URL、请求数据(请求头、请求体)
2、响应报文关键数据
状态响应码、响应数据(响应体)
postman的基础使用
安装
1、下载https://www.postman.com/downloads/
2、安装
双击Postman安装包,安装过程全⾃动,不需要任何⼈为⼲预。
安装完成,默认打开英⽂注册⻚⾯ (如没有跳转,⼿动进⼊注册⻚⾯)
注意
Postman⼀旦安装成功,不要轻易卸载!
不要轻易卸载!不要轻易卸载!不要轻易卸载!
Postman有BUG,默认不⽀持同⼀版本重复安装。
如果重复安装,新安装的版本号 必须 ⼤于已卸载版本才⾏。否则⽆法安装成功
填写:邮箱、⽤户名、密码,完成注册。注意⽤户名要复合Postman要求的规范
(经常会因为⽤户名已存在,注册失败,重新改换⽤户名,再重新注册)。
启动 postman,使⽤⾃⼰注册的账号登录。
页面大致如下(可能略有不同)。
注册成功后,只能在浏览器中打开postman,⽽不能在 app中打开,可尝试如下操作。
- 在 app 中 点击 “sign in
2. 跳转⾄浏览器后,在⻚⾯选择你⾃⼰的账号
3. 在 弹出的⻚⾯中,选择在 postman 这个 app 中打开
4. 成功使⽤账号密码登录,会在 app 有响应图标提示
安装Postman插件newman
要想给 postman 安装 newman 插件,必须 先 安装 node.js。 这是前提!
- 安装node.js
可能你使⽤的电脑,曾经安装过 node.js。先测试下,有没有。
cmd 打开命令提示符,输⼊命令 npm -v,如果能查看到npm的版本号信息(具体版本号是多少⽆所谓),
可跳过 “1. 安装node.js” 这步,直接看 “ - 安装 newman
安装newman前,必须保证 node.js 已经装成功!!!
在线安装命令:npm install -g newman
注意:使⽤ “管理员” 身份,启动 命令提示符,写⼊命令。
如果安装失败。避开⽹络访问⾼峰期(早7:30~9:30 午11:30~14:30 晚18:30~22:30)再安装,尝试 10⼏次 或 ⼏
⼗次 也是有可能的!
注意:Windows 终端 有缺陷!安装过程中,不要⽤⿏标 点击 终端⻚⾯(别⽤⿏标点⿊窗⼝⾥⾯)。
安装newman-reporter-htmlextra
安装命令:npm install -g newman-reporter-htmlextra
注意:使⽤ “管理员” 身份,启动 命令提示符,写⼊命令。
只要 newman 安装成功,此插件⼀定能安装成功,如失败,可多尝试⼏次。
扩展
Git安装
1、下载安装包
2、双击安装
3、一路next,全部使用默认选项,不建议修改安装目录位置,否则后面使用时pycharm会找不到
4、查看是否安装成功,cmd打开终端。输入git --version看到版本信息,说明安装成功
5、必须给git配置用户名和油箱,双引号包裹
git config --global user.email "邮箱名称@xxx.com"
git config --global user.name "有户名(可任意,不要写中文)
注册Gitee码云账号
1、使用163邮箱作为用户名,注册云账号,并激活https://www.git.com
新建仓库:
能创建仓库,说明注册成功
看到自己注册的邮箱,说明注册成功
持续集成
概念:
团队成员将自己的工作成果,持续集成到一个公共平台的过程。成员可以每天集成一次,也可以一天集成多次。
相关工具:
本地代码管理:git
远程代码管理:gitee(国内)、github(国外)、gitlib(公司私有服务器)
持续集成:jenkins
git
简介和安装
安装:
双击 exe文件,一路 next 自动安装,全部默认选项。 (不建议修改默认安装目录)
查看:
cmd打开终端,输入 git --version 能看到版本信息,说明安装成功
配置:
安装成功后,打开 cmd 输入命令。 这一步必须操作!!! 操作后无返回结果!!!
git config --global user.email “你⾃⼰的邮箱名@xxx.com”
git config --global user.name “⽤户名(可任意写,不要⽤中⽂)”
简介:
git 是一款 在 本地 管理代码的工具。 自带一个小型 仓库,存储本地代码。
Gitee
注册网易邮箱
注册 Gitee 账号
新建仓库
git 和 gitee 管理代码工作原理
PyCharm 配置 Gitee 插件
PyCharm 与 Gitee 相关操作
-
将 Gitee的项目 Checkout到 Pycharm中
使用场景:
第一次加入某个项目,第一次从 gitee 获取代码时。
-
推送 PyCharm 新项目到 Gitee远程仓库
应用场景:
本地开发的项目,第一次上传到 gitee 中。
-
将 Pycharm代码 push到 Gitee远程仓库
应用场景:
本地 和 gitee 有 相同的项目。 本地代码做了新增。需要将新增的代码,推送gitee上。
-
将 Gitee仓库的新代码 pull 到 PyCharm中
应用场景:
本地 和 gitee 有 相同的项目。 gitee上代码做了新增。需要将新增的代码,拿到本地来。
-
解决冲突
应用场景
本地 和 gitee 有 相同的项目。 gitee上代码做了新增。本地对同一处代码,做了不同的新增
jenkins
简介、安装、启动
简介:
基于Java开发(必须安装jdk)的一种开源、跨平台的持续集成工具
必须 安装 jdk,要配置 环境变量。
查验:java -version 能看到 1.8 版 jdk 即可。
启动:
- 在 jenkins.war 文件 所在 目录地址栏 输入 cmd 打开 终端。
- 输入命令 java -jar jenkins.war 启动服务。
- 启动成功后,终端窗口 不能关闭。最小化。
- 在浏览器地址栏 输入 localhost:8080
插件安装介绍
Jenkins左侧菜单栏 —> “Manage Jenkins” —> 选择 “Manage Plugins” —> “可选插件” —> 插件名称
如:“HTML Publisher”
注意:不要随意更新为最新版,可能出现不兼容的问题。
系统设置
配置小结:
- Manage Jenkins —> Configure System
- Jenkins Location:
系统管理员邮件地址: —— 自己申请的邮箱。 (黄色警告,不理) - Extended E-mail Notification:(第一个“高级”)
注意:不要随意更新为最新版,可能出现不兼容的问题。
操作细节见《 Jenkins安装及配置.pdf》中 “配置Jenkins系统邮箱” 小节。
SMTP Username: —— 自己申请的邮箱
SMTP Password: —— POP3/SMTP 服务授权码。 - 邮件通知:(勾选“使用SMTP认证”)
用户名:—— 自己申请的邮箱
密码: —— POP3/SMTP 服务授权码。 - 点击 “应用” —> “保存
持续集成postman
准备工作
- 打开已完成并测试无误的 postman 项目脚本。 再次执行测试。
- 导出( 测试用例集、环境变量 两个文件)“不 支 持 中 文” —— 全部改成英文!
- 文件所在目录地址栏 输入 cmd 打开终端。注意:用 “绝对路径” 测试。方便使用 Jenkins。
- 执行无误, 查看生成的测试报告文件
# extra版报告
newman run 测试用例集名.json -e 环境变量文件.json -d 数据文件.json -r htmlextra --reporterhtmlextra-export 报告名称.html
# 示例:
newman run "C:\Users\xwp\Desktop\postman_jenkins\iHRM.postman_collection.json" -e
"C:\Users\xwp\Desktop\postman_jenkins\iHRM_env.postman_environment.json" -r htmlextra --
reporter-htmlextra-export report.html
# 示例:
newman run "C:\Users\xwp\Desktop\ihrm\ihrm.postman_collection.json" -e
"C:\Users\xwp\Desktop\ihrm\postman_environment.json" -r htmlextra --reporter-htmlextra-export
report.html
使用jenkins管理-手动构建
操作步骤:
- 打开 Jenkins 首页,点击 “新建Item” 创建一个 新任务
- 输入任务名,如:bjtestAPITestIHRMPostman。选择 “Freestyle project”,点 “确定” 。跳至 “配置”页面。
- 回主页,可看到 ,多出任务 bjtestAPITestIHRMPostman。 点 任务名称,“配置” 可以继续刚才的配置。
- 跳至 “构建” 标签。(General 、源码管理、构建触发器、构建环境 四个标签先跳过)
- 点击 “增加构建步骤”,选择 “Execute Windows batch command” 选项(macOS选择 “Execute shell”
) - 将 cmd 终端 测试无误的 命令, 粘入“命令” 编辑框中(如有红色浪线警告,忽略)。
- “构建后操作” 标签。
- 点击 “增加构建后操作步骤”,选择 “Pulish HTML reports”,点击 “新增” 按钮。
- 将 Index page[s] 后的值改为:“report.html” 。名称 应与上面 命令中 生成的 测试报告名称 一致。
说明:
因为生成报告时,没有指定目录。 所以:上面 “HTML directory to archive” 是空的。
如果,指定报告生成到其他位置。 要配置 “HTML directory to archive”的值。
- Report title 是生成的报告 标题,可修改为 “Report666” 试试看。
6.点击应用–>保存
11. 在 自动跳至 页面中,点击 “Build Now”,可在下面 Build History(构建历史)中, 看到构建正在进行。
12. 点 #1 后面的名称 , 点击 “控制台输出” 查看 执行的命令。 2. 再次点 “Build Now”,可以 再构建一次 #2。 以此类推 8. 完成后,左侧菜单中 多出 “Report666” 菜单栏。点击可查看 测试报告。可能会样式错乱。后续解决。
使用Jenkins管理-自动构建
简单来说:自动构建,就是设置一个定时器,定时时间到, Jenkins 自动执行测试用例。
操作步骤:
- Jenkins 首页,点击任务名:如: bjtestAPITestIHRMPostman。跳至 “配置” 页面。
- “构建触发器” 标签。 选择 “Build periodically(定期地)”
- “日程表” 中写入:* * * * *(空格隔分) —> 分别对应 “分 时 日 月 年”
修改:10 16 * * * 就是 每天的 16 点 10 分 执行。 - 点击 “应用” --> “保存”。 等待 。。。自动构建 —— 成功!
- 报告样式错乱 原因:
Jenkins为了避免受到恶意攻击,设置了CSP安全策略。只允许加载 Jenkins服务器上托管的 CSS文件 和
图片文件。
我们的用例、代码都是放在自己的服务器上,被恶意攻击的可能性极低。 - 解决 报告样式错乱:
- 在 启动时,添加参数 :java -Dhudson.model.DirectoryBrowserSupport.CSP= -jar Jenkins.war 2. 如果,已生成的报告,样式依然错乱, 重新 “Build Now” 生成新的报告即可。
持续集成-代码
准备工作
将 运行无误,能生成报告的 iHRM项目代码 上传至 Gitee中。
使用Jenkins管理-手动构建- 打开 Jenkins 首页, 点击 “新建Item” 创建一个新任务。
- 输入任务名,如: bjtestAPITestIHRMCode。选择 “Freestyle project”,点 “确定”,跳至 “配置” 页面。
- “源码管理” 标签。 选择 “Git”。 在 Repository URL 后写入 项目代码在 Gitee的 URL。
- “构建” 标签。
- 点击 “增加构建步骤”, 选择 “Execute Windows batch command” 选项(macOS选择 “Execute shell”
) - 输入命令 python run_suite.py ( 与在 pycharm 的 Terminal 中执行,相同含义)
- “构建后操作” 标签
- 点击 “增加构建后操作步骤”,选择 “Pulish HTML reports”,点击 “新增” 按钮。
- 在 “HTML directory to archive” 中 写入 报告生成的位置。 如:./report (与项目目录一致)
- Index page[s] 后的值,与 run_suite.py 中代码,生成的测试报告名称保持一致。如:ihrm.html。
- 再次点击 “增加构建后操作步骤”, 选择 “Editable Email Notification” 设置 邮件发送测试报告。
- 在 “Project Recipient List” ,$DEFAULT_RECIPIENTS 后使用 英文 “,” 隔分,添加邮箱地址。
- 下面 “Content-Type” 的值,选择 HTML(text/html)
- 复制讲义中 “邮件测试报告模板” 代码到 “Default Content ” 中
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
</head>
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
<div>
<h2>项目信息</h2>
<ul>
<li>项目名称:${PROJECT_NAME}</li>
<li>详细测试报告:<a
href="${PROJECT_URL}HTML_20Report/">${PROJECT_URL}HTML_20Report/</a></li>
<li>触发原因:${CAUSE}</li>
<li>项目Url:<a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
</ul>
<hr/>
<h2>构建日志</h2>
<div>${JELLY_SCRIPT,template="html"}</div>
<hr/>
</div>
</body>
</html>
7. 点击右下角 “Advanced Settings” 按钮。将 “Trigger” 原有的内容 点 最外层 “红叉” 删除。
点击 “Add Trigger” 选择 Always 。
8. 点击 “应用” --> “保存”。
9. 点击 “Build Now” 开始手动构建。
使用Jenkins管理-自动构建
- 点击 “配置” 查看,在 “构建触发器” 标签。 选择 “Build periodically(定期地)” 添加 定时构建!
- “日程表” 中写入:* * * * *(空格隔分) —> 分别对应 “分 时 日 月 年”
修改:10 16 * * * 就是 每天的 16 点 10 分 执行。 - 也可以在 “构建触发器” 标签 中选择 “Poll SCM”。 写入 :*/1 * * * * (空格隔分)
代表1分钟 检查一次 gitee 上的代码,查验是否有更新。 - PyCharm 修改代码 Commit、Push,或者 直接在 Gitee修改,导致 Gitee上的代码有 变动。会触发Jenkins
会自动构建。
postman基础使用
传递查询参数
访问TPshop搜索商品的接⼝,搜索关键字iPhone,并查看响应数据
1、创建新用例集
2、用例集上右键,添加请求
3、在下拉列表中选择请求方式
案例二
【提交表单数据】:使⽤ Postman 向 tpshop 商城 登录接⼝ 发送 登录请求
响应数据
案例三
post发送登录请求【发送json数据】
接口用例设计
接口测试的测试点:
接口测试维度
功能测试
单接⼝功能测试:
⼀个单独的业务,就对⼀个独⽴的接⼝。如:登录业务,对应登录接⼝。
业务场景功能测试:
多个接⼝被连续调⽤。(模拟⽤户的实际使⽤场景)
性能测试
响应时⻓:从发送请求到接收到服务器回发响应包所经历的时间。
错误率:服务器运⾏出错的概率
吞吐量:服务器单位时间内,处理请求的数量。
服务器资源利⽤率:cpu、内存、⽹络、磁盘等 硬件资源的占⽤率
安全测试
攻击安全:木马、病毒…
由具备专业安全技术,会使⽤专业安全测试⼯具的 安全测试⼯程师 负责。
业务安全:
必须登录,才能访问 ⽤户数据。
敏感数据加密存储
SQL注入
接口用例设计方法
单接口测试
⼀个单独的业务,就对⼀个独⽴的接⼝。如:登录业务,对应登录接⼝。注册业务,对应注册接⼝。⽀付业务,对应⽀付接⼝
正向:
必选 参数。 所有必选项,给正确数据
组合 参数。 所有必选 + 任意可选,给正确数据。
全部 参数。 所有必选 + 所有可选。给正确数据。
反向:
功能异常:数据格式正确,不能履⾏接⼝功能。
数据异常:数据格式不正确(空值、特殊字符、汉字、字⺟、⻓度、范围 ---- 等价类、边界值)
参数异常:
多参:多出 必选参数
少参:缺少 必选参数
⽆参:没有 指定参数
错误参数:参数名 错误
业务场景测试
⼀定在单接⼝测试 之后!
尽量模拟⽤户实际使⽤场景。
尽量⽤最少的⽤例,覆盖最多的接⼝请求。
⼀般情况下,覆盖正向测试即可
单接口测试用例案例
登录
正向:
登录成功
反向:
功能异常:
⼿机未注册
密码错误
数据异常:
⼿机号为空
⼿机号含有字⺟、特殊字符
⼿机号12位
⼿机号10位
密码为空
密码含有字⺟、特殊字符
密码1位
密码100位
参数异常:
多参:多出abc
少参:缺少-mobile
⽆参:
错误参数:修改 mobile 为 abc
业务场景用例
分析测试点
指导思想:模拟⽤户实际使⽤,⽤较少的测试⽤例,覆盖更多接⼝,测试正向即可。
登录 - 添加员⼯ - 查询员⼯ - 修改员⼯ - 删除员⼯ - 查询员⼯列表
postman常用断言
断言状态响应码
status code: Code is 200
//断言状态响应码为200
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm:postman的实例
test() postman实例的测试方法。这个方法有两个参数
参数1:Status code is 200 这个参数可以随便修改,不影响断言
作用:在断言结束后,显示给用户,断言结果的提示文字
参数2:是一个匿名函数调用
pm.response.to.have.status(200);
意思是响应的结果中,应该有响应状态码200 —这里的200是预期结果
断言包含某字符串
response body :contatins string
pm.test("Body matches string", function () {
pm.expect(pm.response.text()).to.include("string_you_want_to_search");
});
pm:postman的实例
test() postman实例的测试方法。这个方法有两个参数
参数1:“Body matches string” 这个参数可以随便修改,不影响断言
作用:在断言结束后,显示给用户,断言结果提示的是文字
参数2:是一个匿名函数调用
pm.expect(pm.response.text()).to.include(“string_you_want_to_search”); 的意思是:
postman 期望 响应文本中,应该包含 “你想搜索的字符串”(预期结果)
断言json数据
response body:json value check
pm.test("Your test name", function () {
var jsonData = pm.response.json();
pm.expect(jsonData.value).to.eql(100);
});
pm:postman的实例。
test() postman实例的测试方法。 这个方法 有 2 个参数。
参1:“Body matches string”。 这个参数可以任意修改,不影响 断言。
作用:在断言结束后,显示给用户,断言结果的提示文字。
参2:是一个 匿名函数 调用。
var jsonData = pm.response.json(); 将 整个 json响应体 赋值到 变量 jsonData 上。
pm.expect(jsonData.value).to.eql(100); postman 期望 json结果中 指定key 的值为 xxx
value 能取值:success、code、message
postman断言工作原理
postman关联
简介:当接口和接口之间,有依赖关系时,需要借助 postman 关联技术,来实现。
如: 登录接口 返回的 令牌数据,被 添加员工接口依赖。
添加员工接口 返回 员工id,被 查询员工接口依赖。
实现步骤
假定:接口B 产生的数据,被 接口A 依赖
1、发送接口B请求,获取响应数据
2、将响应数据,放到公共容器(全局变量、环境变量)中
3、接口A从容器中,提取数据、发送请求
核心代码:
代码
//1、获取响应数据,转为json格式,保存到变量jsondata中
var jsonData = pm.reponse.json
//2.1 使用全局变量作为容器
pm.global.set("全局变量名",全局变量值)
//2.2 使用环境变量做容器
pm.environment.set("环境变量名",环境变量值)
//3.在postman界面中(URL、请求头header、请求体body)提取全局变量、环境变量数据
{{全局变量名}}、{{环境变量名}}
创建环境
全局变量:在 整个postman中都可以使用的变量。不需要 单独创建环境。
环境变量:在 特定的环境下,才能使用的变量。需要给此变量创建单独的环境。
1. 使用 postman 关联,实现下面案例
从获取天气接口,http://www.weather.com.cn/data/sk/101010100.html
获取返回结果中的城市名称
调用百度搜索接口: http://www.baidu.com/S?wd=北京 ,把获取到的城市名称,如:北京,作为请求参数
思路:
- 发送 获取天气请求,获取响应结果
- 从响应结果中,拿到城市名,存入 全局变量
- 百度搜索接口从 全局变量中,取城市名,发送搜索请求。
//1、获取响应结果
var jsonData = pm.reponse.json()
//2、 在响应结果中,提取城市名
var city = jsonData.weatherinfo.city
//3、 将城市保存到全局变量中
pm.globals.set("glb_city",city)
案例2
使用 postman 关联技术,实现 添加员工 接口。
登录成功,返回的 “令牌” 被 添加员工 接口依赖。
思路:
- 发送登录请求(必须登录成功),获取响应结果
- 从 json 响应结果中,提取 data 值。拼接上 “Bearer ” 前缀。
- 将拼接无误的 令牌,存入 环境变量。 从 “眼睛” 图标查看。
- 添加员工 接口,从 环境变量 中,提取 令牌。设置到请求头中,作为 Authorization 的 值。
- 填写 添加员工 接口 其他信息(post、URL、请求体),发送请求。
postman参数化
简介
什么是参数化:
将 测试数据,组织到数据文件中,通过脚本的反复迭代,使用不同的数据,达到测试不同用例的目标。
应用场景:
一般在测试同一个接口的不同 测试点时,只有测试数据不同。考虑使用 参数化。
数据文件简介
CSV:
优点:数据组织格式简单
缺点:
- 不能测试 bool 类型。因为 postman 读取 csv后,将所有非数值类型数据,自动添加 ”“ 变为字符串
- 不能存储复杂数据类型(元组、列表、字典)。
- 不能实现 参数测试。
应用场景:数据量较大,数据组织格式简单
JSON:
优点: - 可以测试 bool类型
- 能使用 复杂数据类型
- 可以实现 参数测试。
缺点:相同数据量,json文件要远大于 csv 文件。
应用场景:数据量较少,数据组织格式复杂。需要进行 参数测试!
编写数据文件
CSV文件
json文件
导入数据文件到postman中
读取数据文件数据
理论
根据会用位置不同,有两种方法
第一种:请求参数(请求行、请求头、请求体)中,使用 数据文件中 的数据
csv文件:{{字段名}}; json文件:{{键名}}
第二种:代码(Tests)中,使用 数据文件中 的数据
使用 postman 内置的 关键字 data,索引 字段名 或 键名
csv文件:data.字段名; json文件:data.键名
案例
需求:批量查询 手机号 所属运营商,校验运营商数据正确性
接口: http://cx.shouji.360.cn/phonearea.php?number=13012345678
测试数据:
手机号: 13012345678 运营商: 联通 手机号: 13800001111 运营商: 移动 手机号: 18966778899 运营商: 电
信
在http请求页中使用了数据文件中国的关键字,不能在使用send发送请求
postman控制台调试
json文件参数化
安装postman插件newman
导出用例集
导出
扩展,导入用例集
导出环境文件
如果,测试用例脚本中,包含 环境使用。必须要导出 环境文件!!!
newman生成测试报告
完整命令
newman run 用例集文件.json -e 环境文件.json -d 数据文件.json/.csv -r htmlextra --reporterhtmlextra-export 测试报告名.html
-e 和 -d 是 非必须的。
如果没有使用 环境,不需要指定 -e
如果没有使用 数据文件(做参数化),不需要指定 -d
实例1:指定用例集和数据文件
示例2:指定用例集文件和环境文件
环境安装检查
案例练习
初始化项目环境
新建用例集
创建环境
登录模块
登录成功接口
请求接口
添加断言
其他接口共性分析
- 由于是同一个接口,因此:请求方法、URL、请求头 完全一致。
- 测试点(测试用例名称)、和 请求数据(请求体),各不相同
- 响应结果(用作断言),共 3 种情况
- 操作成功
- 用户名或密码错误
- 抱歉,系统繁忙…
实现其他接口
员工管理业务场景
总析
共有两种依赖:
- 登录成功的 令牌, 被 添加、修改、删除、查询 接口依赖。
- 添加员工成功 得到的 员工id,被 修改、删除、查询 接口依赖。
提取令牌
代码写在 “登录成功”接口请求 的 Tests 标签页中
添加员工
注意:
- 登录的令牌,在 请求头中使用
- 请求体中的手机号,要保证唯一。
提取添加员工的id
代码写在 “添加员工成功” 接口请求 的 Tests 标签页中
// 获取添加员工成功的 响应结果 json
var jsonData = pm.response.json()
// 提取 员工id
var emp_id = jsonData.data.id
// 设置到 环境变量
pm.environment.set("env_emp_id", emp_id)
查询员工
其他员工操作
批量运行测试用例
注意:业务场景,批量执行之前,必须修改 “添加员工”接口使用的手机号,否则 ,查询、修改、删除 都无法正
常批量运行。
生成测试报告
登录接口生成测试报告:
- 导出用例集文件。(没有使用环境,不需要导出环境文件)
- 使用命令 生成测试报告
newman run ihrm项目.postman_collection.json -r htmlextra --reporter-htmlextra-export ihrm登录接口
测试报告.html
带有业务场景接口,生成测试报告:
- 导出用例集文件。
newman run ihrm项目.postman_collection.json -r htmlextra --reporter-htmlextra-export ihrm登录接口
测试报告.html - 必须 要导出 环境文件。
- 使用命令 生成测试报告
newman run ihrm项目.postman_collection.json -e 测试环境.postman_environment.json -r htmlextra –
reporter-htmlextra-export ihrm项目完整测试报告.html.
注意:由于添加员工时,手机号要求唯一! 因此上述生成报告的命令, 第二次+ 执行,生成的报告中,会有断
言失败
实战练习
创建环境
新建测试集,按照模块和分类进行划分不同的文件夹
request库
简介:
Requests库 是 Python编写的,基于urllib 的 HTTP库,使用方便
安装
方法1:
pip install requests
方法2:
pip install requests -i https://pypi.douban.com/simple/
豆瓣镜像:https://pypi.douban.com/simple/
加上-i参数添加镜像源
检验:
步骤一:pip 中查验
pip list
pip shw 库名
步骤二:pycharm中查验
设置http请求语法
res = requests.请求方法(url = “URL地址”,params={k:v},headers ={k:v},data= {k:v},cookies =‘cookies数据’{如:令牌})
请求方法:
get请求 --get()
post请求 --post()
put请求 --put()
delete请求 – delete()
url :待请求的url -string类型
params:查询参数 -字典
data :表单格式的 请求体 -字典
json:json格式的请求体 – 字典
cookies:cookie数据-string类型
入门案例:使用Requests库访问 百度 http://www.baidu.com
import requests
resp = requests.get(url="http://www.baidu.com")
print(resp.text)
应用案例
案例一
【带 查询参数 的get请求】使用Requests库,请求 tpshop商城 搜索商品接口。查询 iphone
import requests
# 发送get请求,指定url,获取响应结果
# 方法一
# resp =requests.get(url ="http://192.168.13.141/Home/Goods/search.html?q=iPhone")
# print(resp.text)
# 方法二
resp = requests.get(url ="http://192.168.13.141/Home/Goods/search.html",params={"q":"iPhone"})
print(resp.text)
案例二
【带表单数据 的post请求】使用Requests库,完成 tpshop商城 登录接口调用。返回 ”验证码错误“ 即可。
# 发送post请求,指定url,请求头,请求体,获取响应结果
import requests
resp = requests.post("http://192.168.13.141/index.php?m=Home&c=User&a=do_login&t=0.7094195931397276",headers={"Content-Type":"application/x-www-form-urlencoded"},
data={"username":"13012345678","password":"1234567","verify_code":"8888"})
# 打印响应结果-文本
print(resp.text)
# 打印响应结果-json
print(resp.json())
{“status”:0,“msg”:“\u9a8c\u8bc1\u7801\u9519\u8bef”}
{‘status’: 0, ‘msg’: ‘验证码错误’}
案例三
【带 json数据 的post请求】使用Requests库,完成 iHRM系统 成功登录。返回 ”令牌数据“。
import requests
# 发送 post 登录请求,指定 url、请求头、请求体,获取响应结果
resp = requests.post(url="http://ihrm-test.itheima.net/api/sys/login",
# headers={"Content-Type": "application/json"},
json={"mobile": "13800000002", "password": "123456"})
# 打印响应结果
print(resp.json())
案例四
【发送 put、delete请求】使用Requests库发送 ihrm系统 修改员工信息、删除员工信息 请求。
# -------- 修改 put
import requests
resp = requests.put(url="http://ihrm-test.itheima.net/api/sys/user/1467780995754229760",
headers={"Authorization": "Bearer 4c51c601-c3f7-4d1a-a738-7848f2439f45"},
json={"username": "齐天大圣"})
print(resp.json())
# -------- 删除 delete
import requests
resp = requests.delete(url="http://ihrm-test.itheima.net/api/sys/user/1467780995754229760",
headers={"Authorization": "Bearer 4c51c601-c3f7-4d1a-a738-7848f2439f45"})
print(resp.json())
cookie
简介
简介:工程师针对 http协议是无连接、无状态特性,设计的 一种技术。可以在浏览器端 存储用户的信息。
特性:
cookie 用于存储 用户临时的不敏感信息。
cookie 位于浏览器(客户端)端。默认大小4k(可以调整)
cookie 中的数据,可以随意被访问,没有安全性可言。
cookie 中存储的数据类型, 受浏览器限制。
Cookie+Session认证方式
在计算机中,认证用户身份的方式有多种!课程中接触 2种:
ihrm项目:token认证。
tpshop项目:cookie+Session认证
案例5
完整实现 TPshop商城登录,并获取 “我的订单” 页面数据。
获取验证码:http://tpshop-test.itheima.net/index.php?m=Home&c=User&a=verify
登录:http://tpshop-test.itheima.net/index.php?m=Home&c=User&a=do_login
我的订单:http://tpshoptest.itheima.net/Home/Order/order_list.html
import requests
# 发送验证码请求
resp_v = requests.get(url="http://192.168.13.141/index.php?m=Home&c=User&a=verify&r=0.0.21519623710645064")
# 从获取短信验证码的响应结果中,提取cookie
my_cookie = resp_v.cookies
# 发送登录请求url/请求头、请求体,携带cookie,得到响应结果
resp = requests.post("http://192.168.13.141/index.php?m=Home&c=User&a=do_login&t=0.7094195931397276", data={"username":"13012345678","password":"1234567","verify_code":"8888"},cookies = my_cookie)
print(resp.json())
# 发送查看我的订单请求
resp_o = requests.get(url ="http://192.168.13.141/Oreder/order_list.html",cookies =my_cookie)
print(resp_o.text)
session
简介
简介:也叫会话。通常出现在网络通信中,从客户端借助访问终端登录上服务器,直到 退出登录 所产生的通信数据,保存在会话中。
特性:
Session 用于存储用户的信息。
Session 位于服务端。大小直接使用服务器存储空间
Session 中的数据,不能随意被访问,安全性较高。
Session 中存储的数据类型,受服务器影响,几乎能支持所有的数据类型。
Session自动管理Cookie
因为 Cookie 中的数据,都是 Session 传递的。因此,Session可以直接自动管理 cookie
案例6
借助session重新实现 上述 TPshop商城登录,并获取 “我的订单” 页面数据。
实现步骤:
- 创建一个 Session 实例。
- 使用 Session 实例,调 get方法,发送 获取验证码请求。(不需要获取cookie)
- 使用 同一个 Session 实例,调用 post方法,发送 登录请求。(不需要携带 cookie)
- 使用 同一个 Session 实例,调用 get方法,发送 查看我的订单请求。(不需要携带 cookie)
import requests
# 1、创建一个session实例
session = requests.Session()
# 2、使用session实例,调用get方法,发送获取验证码请求(不需要获取cookie)
resp_v = session.get(url="http://192.168.13.141/index.php?m=Home&c=User&a=verify&r=0.0.21519623710645064")
# 3、使用同一个session实例,调用post方法,发送登录请求(不需要获取cookie)
resp = session.post(url="http://192.168.13.141/index.php?m=Home&c=User&a=do_login&t=0.7094195931397276", data={"username":"13012345678","password":"1234567","verify_code":"8888"})
print(resp.json())
# 4、使用同一个session实例,调用get方法,发送登录请求,发送查看我的订单请求
resp_o =session.get(url ="http://192.168.13.141/Oreder/order_list.html")
print(resp_o)
cookie和session的区别
- 数据存储位置:
cookie存储在浏览器;session存储在服务器。 - 安全性:
cookie中的数据可以随意获取,没有安全性可言。Session的数据多为加密存储,安全较高! - 数据类型:
cookie支持的数据类型受浏览器限制,较少;Session直接使用服务器存储,支持所有数据类型 - 大小:
cookie大小默认 4k; Session 大小约为服务器存储空间大小
获取指定响应数据
常用:
获取url:resp.url
获取响应状态码:resp.status_code
获取cookie:resp.cookies
获取响应头:resp.headers
获取响应体:
文本格式:resp.text
json格式:resp.json()
import requests
resp = requests.get(url="http://www.baidu.com")
# 获取url
print("url =",resp.url)
# 获取响应状态码
print("status_code=",resp.status_code)
# 获取cookie
print("cookies=",resp.cookies)
# 获取响应头
print("header=")
# 获取响应体-文本格式
print("body_text=",resp.text)
# 获取响应体-json格式,当显示jsondecodeerror错误时,说明resp不能转换为json格式
#print("body_json =",resp.json())
UnitTest框架
UnitTest 是开发人员用来实现 “单元测试” 的框架。测试工程师,可以在自动化 “测试执行” 时使用。
使用 UnitTest 的好处:
- 方便管理、维护测试用例。
- 提供丰富的断言方法。
- 生成测试报告。(需要插件 HTMLTestReport)
TestCase
# 1、导包 import unittest
# 2、定义测试类从 TestCase类继承
class TestXXX(unittest.TestCase):
pass
# 3、测试方法定义必须以test开头,建议添加编号
class TestXXX(unittest.TestCase):
def test01_xxx(self):
pass
Fixture
1、方法级别的setUp(self)tearDown(self)每个普通方法执行前后自动运行
2、类级别的setUpClass(cls)tearDownClass(cls)在类内所有方法之前、之后运行一次
TestSuite
1、实例化测试集对象 suite = unitest.TestSuite()
2、添加指定类的全部测试方法
suite.addTest(unittest.makeSuite(类名))
TestSuite 通过搜索创建测试集
suite = unittest.TestLoader().discover(搜索目录,搜索文件名)
suite = unittest.TestLoader().discover(“./”,"test*.py)
TestRunner
runner = HTMLTestReport(“./report1.html”,description=“描述信息”,title ="报告标题”)
runner.run(suite)
UnitTest框架基础代码回顾
示例
练习案例
登录成功
import unittest
import requests
# 定义测试类
class TestIhrmLogin(unittest.TestCase):
# 添加测试方法
def test01_login_ok(self):
# 发送 post 登录请求,指定 url、请求头、请求体,获取响应结果
resp = requests.post(url="http://ihrm-test.itheima.net/api/sys/login",
json={"mobile": "13800000002", "password": "123456"})
# 打印响应结果
print(resp.json())
# 断言- 响应状态码为 200
self.assertEqual(200, resp.status_code)
# 断言 success 的值为 true
self.assertEqual(True, resp.json().get("success"))
# 断言 code 的值为 10000
self.assertEqual(10000, resp.json().get("code"))
# 断言 message 的值为 操作成功!
self.assertIn("操作成功", resp.json().get("message"))
断言方法:
assertEqual(参1,参2) :
参1:预期结果。 参2:实际结果。
成功:完全相等。断言通过。不报错!
失败:报错!
assertIn(参1,参2):
参1:预期结果。参2:实际结果。
成功:实际结果中,包含预期结果。断言通过。不报错!
失败:报错!
数据库操作应用场景
检验测试数据
接口发送请求后明确会对数据库中的某个字段进行修改,但响应结果中无该字段数据时。
如:ihrm 删除员工接口。 is_delete 字段,没有在响应结果中出现! 需要 借助数据库校验!
构造测试数据
测试数据使用一次就失效。
如:ihrm 添加员工接口,使用的手机号!
测试前,无法保证测试数据是否存在。
如:ihrm 查询员工接口,使用的员工id
PyMySql操作数据库
安装
方法1:
pip install PyMySql
方法2:
pip install PyMySQL -i https://pypi.douban.com/simple/
操作步骤
1、导包 import pymysql
2、创建连接 conn = pymysql.connect(host,port,user,password,database,charset)
3、获取游标 cursor = conn.cursor()
4、执行sql cursor.execute(“sql语句”)
查询语句(select):
处理结果集(提取数据fetch*)
增删改语句(insert/update/delete)
成功:提交事务conn.commit()
失败:回滚事务conn.rollback()
5、关闭游标。cursor.close()
6、关闭连接。conn.close()
事务的概念
事务:是关系系数据库(MySQL)特有的概念
事务,可以看做一个虚拟的容器,在容器中存放一系列的数据库操作,看做一个整体。内部的所有操作,要么一次性成功,只要有一个失败,就全部失败
事务操作:只有两种情况
提交:conn.commit()
回滚:conn.rollback()
PyMySql连接数据库
建立连接方法
conn = pymysql.connect(host=“”, port=0,
user=“”, password=“”, database=“”, charset=“”)
host:数据库所在主机 IP地址 - string
port:数据库使用的 端口号 - int
user:连接数据库使用的 用户名 - string
password:连接数据库使用的 密码 - string
database:要连接的那个数据库的名字 - string
charset:字符集。常用 utf8 - string
conn:连接数据库的对象。
入门案例
查询数据库,获取MySQL服务器版本信息
# 1. 导包
import pymysql
# 2. 建立连接
conn = pymysql.connect(host="211.103.136.244", port=7061, user="student",
password="iHRM_student_2021", database="test_db", charset="utf8")
# 3. 获取游标
cursor = conn.cursor()
# 4. 执行 sql 语句(查询)
cursor.execute("select version()")
# 5. 获取结果
res = cursor.fetchone()
print("res =", res[0])
# 6. 关闭游标
cursor.close()
# 7. 关闭连接
conn.close()
pymsql操作数据库
sql语法回顾
查询语法:
select 字段1,字段2,… from 表 where 条件;
示例:select id,title, pub_date from t_book where title = ‘读者’;
添加语法:
insert into 表名(字段1, 字段2, …) values(值1, 值2, …);
示例:insert into t_book(id, title, pub_date) values(17, ‘红楼梦’, ‘2021-11-11’);
更新语法;
update 表名 set 字段名 = 字段值 where 条件
示例:update t_book set title = ‘三国’ where id = 17;
删除语法:
delete from 表名 where 条件
示例:delete from t_book where title = ‘三国’;
数据库查询
查询操作流程
cursor游标
常用方法
fetchone():从结果集中,提取一行。
fetchmany(size):从结果集中,提取 size 行。
fetchall():提取所有结果集。
属性rownumber:可以设置游标位置。
案例:
查询t_book表,获取 第一条 数据
查询t_book表,获取 前两条 数据
查询t_book表,获取 全部 数据
查询t_book表,获取 第3条和第4条 数据
# 1. 导包
import pymysql
# 2. 建立连接
conn = pymysql.connect(host="211.103.136.244", port=7061, user="student",
password="iHRM_student_2021", database="test_db", charset="utf8")
# 3. 获取游标
cursor = conn.cursor() # 指向 0 号位置。
# 4. 执行 sql 语句(查询)--- t_book
cursor.execute("select * from t_book;")
# 5. 获取结果 - 提取第一条
res1 = cursor.fetchone()
print("res1 =", res1)
# 修改游标位置:回零
cursor.rownumber = 0
# 5. 获取结果 - 提取前 2 条
res2 = cursor.fetchmany(2)
print("res2 =", res2)
# 修改游标位置:回零
cursor.rownumber = 0
res3 = cursor.fetchall()
print("res3 =", res3)
# 修改游标位置:指向第 2 条记录
cursor.rownumber = 2
res4 = cursor.fetchmany(2)
print("res4 =", res4)
# 6. 关闭游标
cursor.close()
# 7. 关闭连接
conn.close()
异常捕获
try:
尝试执行的代码
except Exception as err:
有错误出现时,执行的代码
finally:
无论有没有错误,都会执行的代码
# 1. 导包
import pymysql
# 定义全局变量,初值为 None
conn = None
cursor = None
try:
# 2. 建立连接
conn = pymysql.connect(host="211.103.136.244", port=7061, user="student",
password="iHRM_student_2021", database="test_db", charset="utf8")
# 3. 获取游标
cursor = conn.cursor() # 指向 0 号位置。
# 4. 执行 sql 语句(查询)--- t_book
cursor.execute("select * from t_book;")
# 5. 获取结果 - 提取第一条
res1 = cursor.fetchone()
print("res1 =", res1)
# 修改游标位置:回零
cursor.rownumber = 0
# 5. 获取结果 - 提取前 2 条
res2 = cursor.fetchmany(2)
print("res2 =", res2)
# 修改游标位置:回零
cursor.rownumber = 0
res3 = cursor.fetchall()
print("res3 =", res3)
# 修改游标位置:指向第 2 条记录
cursor.rownumber = 2
res4 = cursor.fetchmany(2)
print("res4 =", res4)
except Exception as err:
print("查询语句执行出错:", str(err))
finally:
# 6. 关闭游标
cursor.close()
# 7. 关闭连接
conn.close()
数据库UID
更新操作
单独实现如下操作: ①:新增一条图书数据(id:5 title:西游记 pub_date:1986-01-01 ) ②:把图书名称
为‘西游记’的阅读量加一 ③:删除名称为‘西游记’的图书
插入数据
"""
新增一条图书数据(id:5 title:西游记 pub_date:1986-01-01 )
insert into t_book(id, title, pub_date) values(5, '西游记', '1986-01-01');
1. 导包
2. 创建连接
3. 获取游标
4. 执行 insert 语句
5. 提交/回滚事务
6. 关闭游标
7. 关闭连接
"""
# 1. 导包
import pymysql
# 定义全局变量
conn = None
cursor = None
try:
# 2. 创建连接
conn = pymysql.connect(host="211.103.136.244", port=7061, user="student",
password="iHRM_student_2021",
database="test_db", charset="utf8")
# 3. 获取游标
cursor = conn.cursor()
# 4. 执行 insert 语句
cursor.execute("insert into t_book(id, title, pub_date) values(175, '西游记', '1986-01-
01');")
# 查看 sql执行,影响多少行
print("影响的行数:", conn.affected_rows())
# 5. 提交事务
conn.commit()
except Exception as err:
print("插入数据错误:", str(err))
# 回滚事务
conn.rollback()
finally:
# 6. 关闭游标
cursor.close()
# 7. 关闭连接
conn.close()
修改数据:
"""
把图书名称为‘西游记’的阅读量加一
update t_book set `read` = `read` + 1 where id = 6;
1. 导包
2. 建立连接
3. 获取游标
4. 执行 update语句
5. 提交、回滚事务
6. 关闭游标
7. 关闭连接
"""
# 1. 导包
import pymysql
conn = None
cursor = None
try:
# 2. 建立连接
conn = pymysql.connect(host="211.103.136.244", port=7061, user="student",
password="iHRM_student_2021",
database="test_db", charset="utf8")
# 3. 获取游标
cursor = conn.cursor()
# 4. 执行 update语句。字段名,需要使用 反引号(`)包裹
cursor.execute("update t_book set `read` = `read` + 1 where id = 1023;")
print("影响的行数:", conn.affected_rows())
# 5. 提交、回滚事务
conn.commit()
except Exception as err:
print("更新失败:", str(err))
# 回滚事务
conn.rollback()
finally:
# 6. 关闭游标
cursor.close()
# 7. 关闭连接
conn.close()
删除数据
"""
删除名称为‘西游记’的图书
delete from t_book where title = '西游记';
1. 导包
2. 建立连接
3. 获取游标
4. 执行 delete 语句
5. 提交、回滚事务
6. 关闭游标
7. 关闭连接
"""
# 1. 导包
import pymysql
conn = None
cursor = None
try:
# 2. 建立连接
conn = pymysql.connect(host="211.103.136.244", port=7061, user="student",
password="iHRM_student_2021",
database="test_db", charset="utf8")
# 3. 获取游标
cursor = conn.cursor()
# 4. 执行 delete语句。
cursor.execute("delete from t_book where id = 151;")
print("影响的行数:", conn.affected_rows())
# 5. 提交、回滚事务
conn.commit()
except Exception as err:
print("更新失败:", str(err))
# 回滚事务
conn.rollback()
finally:
# 6. 关闭游标
cursor.close()
# 7. 关闭连接
conn.close()
数据库工具类封装
封装的目的
将 常用的数据库操作,封装到 一个方法。 后续再操作数据库时,通过调用该方法来实现。
提高代码的复用性!
设计数据库工具类
# 封装数据库工具类
class DBUtil(object):
@classmethod
def __get_conn(cls):
pass
@classmethod
def __close_conn(cls):
pass
# 常用方法:查询一条
@classmethod
def select_one(cls, sql):
pass
# 常用方法:增删改
@classmethod
def uid_db(cls, sql):
pass
实现类方法
获取、关闭连接
# 封装数据库工具类
class DBUtil(object):
# 添加类属性
conn = None
@classmethod
def __get_conn(cls):
# 判断 conn 是否为空, 如果是,再创建
if cls.conn is None:
cls.conn = pymysql.connect(host="211.103.136.244", port=7061, user="student",
password="iHRM_student_2021", database="test_db",
charset="utf8")
# 返回 非空连接
return cls.conn
@classmethod
def __close_conn(cls):
# 判断,conn 不为空,需要关闭。
if cls.conn is not None:
cls.conn.close()
cls.conn = None
查询一条记录
# 封装数据库工具类
class DBUtil(object):
# 常用方法:查询一条
@classmethod
def select_one(cls, sql):
cursor = None
res = None
try:
# 获取连接
cls.conn = cls.__get_conn()
# 获取游标
cursor = cls.conn.cursor()
# 执行 查询语句
cursor.execute(sql)
# 提取一条结果
res = cursor.fetchone()
except Exception as err:
print("查询sql错误:", str(err))
finally:
# 关闭游标
cursor.close()
# 关闭连接
cls.__close_conn()
# 将查询sql执行的 结果,返回
return res
if __name__ == '__main__':
res = DBUtil.select_one("select * from t_book;")
print("查询结果为:", res)
增删改数据
# 封装数据库工具类
class DBUtil(object):
# 常用方法:增删改
@classmethod
def uid_db(cls, sql):
cursor = None
try:
# 获取连接
cls.conn = cls.__get_conn()
# 获取游标
cursor = cls.conn.cursor()
# 执行 uid 语句
cursor.execute(sql)
print("影响的行数:", cls.conn.affected_rows())
# 提交事务
cls.conn.commit()
except Exception as err:
# 回滚事务
cls.conn.rollback()
print("增删改 SQL 执行失败:", str(err))
finally:
# 关闭游标
cursor.close()
# 关闭连接
cls.__close_conn()
if __name__ == '__main__':
DBUtil.uid_db("update t_book set is_delete = 1 where id = 1111;")
完整封装代码实现
import pymysql
# 封装数据库工具类
class DBUtil(object):
# 添加类属性
conn = None
@classmethod
def __get_conn(cls):
# 判断 conn 是否为空,如果是,再创建
if cls.conn is None:
cls.conn = pymysql.connect(host="211.103.136.244", port=7061, user="student",
password="iHRM_student_2021", database="test_db",
charset="utf8")
# 返回 非空连接
return cls.conn
@classmethod
def __close_conn(cls):
# 判断,conn 不为空,需要关闭。
if cls.conn is not None:
cls.conn.close()
cls.conn = None
# 常用方法:查询一条
@classmethod
def select_one(cls, sql):
cursor = None
res = None
try:
# 获取连接
cls.conn = cls.__get_conn()
# 获取游标
cursor = cls.conn.cursor()
# 执行 查询语句
cursor.execute(sql)
# 提取一条结果
res = cursor.fetchone()
except Exception as err:
print("查询sql错误:", str(err))
finally:
# 关闭游标
cursor.close()
# 关闭连接
cls.__close_conn()
# 将查询sql执行的 结果,返回
return res
# 常用方法:增删改
@classmethod
def uid_db(cls, sql):
cursor = None
try:
# 获取连接
cls.conn = cls.__get_conn()
# 获取游标
cursor = cls.conn.cursor()
# 执行 uid 语句
cursor.execute(sql)
print("影响的行数:", cls.conn.affected_rows())
# 提交事务
cls.conn.commit()
except Exception as err:
# 回滚事务
cls.conn.rollback()
print("增删改 SQL 执行失败:", str(err))
finally:
# 关闭游标
cursor.close()
# 关闭连接
cls.__close_conn()
if __name__ == '__main__':
res = DBUtil.select_one("select * from t_book;")
print("查询结果为:", res)
DBUtil.uid_db("update t_book set is_delete = 1 where id = 1111;")
接口对象封装,解决问题
代码冗余度⾼(有⼤量重复代码)
代码耦合度⾼
代码维护成本⾼
核心思想
代码分层
分层思想:
将普通方法实现的,分为接口对象层和测试脚本层
接口对象层:
对接口进行封装。封装好之后,给测试用例层调用
面向对象类封装实现
测试用例层
调用接口对象层封装的方法,拿到响应结果,断言进程接口测试
借助unittest框架实现
封装Tpshop商城
普通方式实现
import unittest
import requests
class TestTpshopLogin(unittest.TestCase):
# 测试 登录成功
def test01_login_ok(self):
# 创建 session 实例
session = requests.Session()
# 使⽤实例,调⽤get 发送获取验证码请求
session.get(url="http://tpshop-test.itheima.net/index.php? m = Home & c = User & a = verify & r = 0.21519623710645064")
# 使⽤实例,调⽤post 发送登录请求
resp = session.post(url="http://tpshop-test.itheima.net/index.php?m = Home & c = User & a = do_login & t = 0.7094195931397276 ",
data = {"username": "13012345678", "password": "123456", "verify_code":
"8888"})
print("响应结果 =", resp.json())
# 断⾔:
self.assertEqual(200, resp.status_code)
self.assertEqual(1, resp.json().get("status"))
self.assertEqual("登陆成功", resp.json().get("msg"))
# 测试 ⼿机号不存在
def test02_tel_not_exists(self):
# 创建 session 实例
session = requests.Session()
# 使⽤实例,调⽤get 发送获取验证码请求
session.get(url="http://tpshop-test.itheima.net/index.php?m = Home & c = User & a = verify & r = 0.21519623710645064")
# 使⽤实例,调⽤post 发送登录请求
resp = session.post(url="http://tpshop-test.itheima.net/index.php?m = Home & c = User & a = do_login & t = 0.7094195931397276",
data = {"username": "13847834701", "password": "123456", "verify_code":
"8888"})
print("响应结果 =", resp.json())
# 断⾔:
self.assertEqual(200, resp.status_code)
self.assertEqual(-1, resp.json().get("status"))
self.assertEqual("账号不存在!", resp.json().get("msg"))
# 测试 密码错误
def test03_pwd_err(self):
# 创建 session 实例
session = requests.Session()
# 使⽤实例,调⽤get 发送获取验证码请求
session.get(url="http://tpshop-test.itheima.net/index.php?m = Home & c = User & a = verify & r = 0.21519623710645064")
# 使⽤实例,调⽤post 发送登录请求
resp = session.post(
url="http://tpshop-test.itheima.net/index.php?m = Home & c = User & a = do_login & t = 0.7094195931397276",
data = {"username": "13012345678", "password": "123456890",
"verify_code": "8888"})
print("响应结果 =", resp.json())
# 断⾔:
self.assertEqual(200, resp.status_code)
self.assertEqual(-2, resp.json().get("status"))
self.assertEqual("密码错误!", resp.json().get("msg"))
登录接口对象层
封装思想:
将动态变化的数据,设计到方法的参数
将固定不变的,直接写成方法实现
将响应结果,通过返回值传出
分析
封装实现
class TpshopLoginApi(object):
# 发送验证码请求
@classmethod
def get_verify(cls, session):
session.get(url="http://tpshop-test.itheima.net/index.php?m=Home&c=User&a=verify&r=0.21519623710645064")
# 发送登录请求
@classmethod
def login(cls, session, login_data):
resp = session.post(
url="http://tpshop-test.itheima.net/index.php?m=Home&c=User&a=do_login&t=0.7094195931397276",
data=login_data)
return resp
登录接口测试用例层
优化前
import unittest
import requests
from tpshop_login_api import TpshopLoginApi
class TestTpshopLogin(unittest.TestCase):
# 测试 登录成功
def test01_login_ok(self):
# 创建 session实例
s = requests.Session()
# ⽤实例,调⽤⾃⼰封装的 获取验证码 接⼝
TpshopLoginApi.get_verify(s)
# 调⽤ ⾃⼰封装的接⼝,登录
abc = {"username": "13012345678", "password": "123456", "verify_code":"8888"}
resp = TpshopLoginApi.login(s, abc)
print(resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(1, resp.json().get("status"))
self.assertEqual("登陆成功", resp.json().get("msg"))
# 测试 ⼿机号不存在
def test02_tel_not_exists(self):
# 创建 session实例
s = requests.Session()
# ⽤实例,调⽤⾃⼰封装的 获取验证码 接⼝
TpshopLoginApi.get_verify(s)
# 调⽤ ⾃⼰封装的接⼝,登录
abc = {"username": "13048932745", "password": "123456", "verify_code":"8888"}
resp = TpshopLoginApi.login(s, abc)
print(resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(-1, resp.json().get("status"))
self.assertIn("账号不存在", resp.json().get("msg"))
# 测试 密码错误
def test03_pwd_err(self):
# 创建 session实例
s = requests.Session()
# ⽤实例,调⽤⾃⼰封装的 获取验证码 接⼝
TpshopLoginApi.get_verify(s)
# 调⽤ ⾃⼰封装的接⼝,登录
abc = {"username": "13012345678", "password": "123456789", "verify_code":"8888"}
resp = TpshopLoginApi.login(s, abc)
print(resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(-2, resp.json().get("status"))
self.assertIn("密码错误", resp.json().get("msg"))
优化后:
import unittest
import requests
from tpshop_login_api import TpshopLoginApi
# 封装 通⽤ 的 断⾔函数
def common_assert(self, resp, status_code, status, msg):
self.assertEqual(status_code, resp.status_code)
self.assertEqual(status, resp.json().get("status"))
self.assertIn(msg, resp.json().get("msg"))
class TestTpshopLogin(unittest.TestCase):
# 添加类属性
session = None
@classmethod
def setUpClass(cls) -> None: # 在 类中 所 有 测试⽅法执⾏之前,⾃动执⾏1次。
cls.session = requests.Session()
def setUp(self) -> None: # 在 每个 测试⽅法执⾏之前,⾃动执⾏1次。
# 调⽤ ⾃⼰封装的接⼝,获取验证码
TpshopLoginApi.get_verify(self.session)
# 测试 登录成功
def test01_login_ok(self):
# 调⽤ ⾃⼰封装的接⼝,登录
data = {"username": "13012345678", "password": "123456", "verify_code":
"8888"}
resp = TpshopLoginApi.login(self.session, data)
# 断⾔
common_assert(self, resp, 200, 1, "登陆成功")
# 测试 ⼿机号不存在
def test02_tel_not_exists(self):
# 调⽤ ⾃⼰封装的接⼝,登录
data = {"username": "13048392845", "password": "123456", "verify_code":
"8888"}
resp = TpshopLoginApi.login(self.session, data)
# 断⾔
common_assert(self, resp, 200, -1, "账号不存在")
# 测试 密码错误
def test03_pwd_err(self):
# 调⽤ ⾃⼰封装的接⼝,登录
data = {"username": "13012345678", "password": "123456890", "verify_code":
"8888"}
resp = TpshopLoginApi.login(self.session, data)
# 断⾔
common_assert(self, resp, 200, -2, "密码错误")
封装断言方法:
# 封装 通⽤ 的 断⾔函数
def common_assert(self, resp, status_code, status, msg):
self.assertEqual(status_code, resp.status_code)
self.assertEqual(status, resp.json().get("status"))
self.assertIn(msg, resp.json().get("msg"))
# 调⽤
common_assert(self, resp, 200, 1, "登陆成功")
common_assert(self, resp, 200, -1, "账号不存在")
common_assert(self, resp, 200, -2, "密码错误")
封装ihrm登录
登录接口
普通方式实现
import unittest
import requests
class TestIhrmLogin(unittest.TestCase):
# 登陆成功
def test01_login_success(self):
resp = requests.post(url="http://ihrm-test.itheima.net/api/sys/login",
json={"mobile": "13800000002", "password": "123456"})
print(resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(True, resp.json().get("success"))
self.assertEqual(10000, resp.json().get("code"))
self.assertIn("操作成功", resp.json().get("message")
登录接口对象层
思路:
动态变化的,写⼊参数
固定不变,直接写到⽅法实现内
响应结果,通过返回值 return
# 接⼝对象层
import requests
class IhrmLoginApi(object):
@classmethod
def login(cls, json_data):
resp = requests.post(url="http://ihrm-test.itheima.net/api/sys/login",
json=json_data)
return resp
登录接口测试用例层
import unittest
from ihrm_login_api import IhrmLoginApi
# 定义测试类
class TestIhrmLogin(unittest.TestCase):
# 测试⽅法 - 登录成功
def test01_login_success(self):
# 调⽤ ⾃⼰封装 login
login_data = {"mobile": "13800000002", "password": "123456"}
resp = IhrmLoginApi.login(login_data)
print("登录成功:", resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(True, resp.json().get("success"))
self.assertEqual(10000, resp.json().get("code"))
self.assertIn("操作成功", resp.json().get("message"))
# 测试⽅法 - ⼿机号未注册
def test02_mobile_not_register(self):
# 调⽤ ⾃⼰封装 login
login_data = {"mobile": "1384780932", "password": "123456"}
resp = IhrmLoginApi.login(login_data)
print("⼿机号未注册:", resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(False, resp.json().get("success"))
self.assertEqual(20001, resp.json().get("code"))
self.assertIn("⽤户名或密码错误", resp.json().get("message"))
# 测试⽅法 - 密码错误
def test03_pwd_error(self):
# 调⽤ ⾃⼰封装 login
login_data = {"mobile": "13800000002", "password": "890"}
resp = IhrmLoginApi.login(login_data)
print("密码错误:", resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(False, resp.json().get("success"))
self.assertEqual(20001, resp.json().get("code"))
self.assertIn("⽤户名或密码错误", resp.json().get("message"))
# 测试⽅法 - ⼿机号为空
def test04_mobile_is_none(self):
# 调⽤ ⾃⼰封装 login
login_data = {"mobile": None, "password": "123456"}
resp = IhrmLoginApi.login(login_data)
print("⼿机号为空:", resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(False, resp.json().get("success"))
self.assertEqual(20001, resp.json().get("code"))
self.assertIn("⽤户名或密码错误", resp.json().get("message"))
# 测试⽅法 - 多参
def test12_more_params(self):
# 调⽤ ⾃⼰封装 login
login_data = {"mobile": "13800000002", "password": "123456", "abc":"123"}
resp = IhrmLoginApi.login(login_data)
print("⼿机号为空:", resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(True, resp.json().get("success"))
self.assertEqual(10000, resp.json().get("code"))
self.assertIn("操作成功", resp.json().get("message"))
# 测试⽅法 - ⽆参
def test14_none_params(self):
# 调⽤ ⾃⼰封装 login
login_data = None
resp = IhrmLoginApi.login(login_data)
print("⼿机号为空:", resp.json())
# 断⾔
self.assertEqual(200, resp.status_code)
self.assertEqual(False, resp.json().get("success"))
self.assertEqual(99999, resp.json().get("code"))
self.assertIn("抱歉,系统繁忙,请稍后重试", resp.json().get("message"))
封装断言方法
- 创建 ⽂件 assert_util.py
- 在 ⽂件内,定义 common_assert() 函数
- 直接粘贴 unittest框架中的断⾔代码,修改参数。
- 回到 unittest框架 实现的 测试脚本中, 调⽤该函数,实现断⾔,传递 实际参数。
# 定义 断⾔ 函数
# def common_assert():
# self.assertEqual(200, resp.status_code) # self / resp 报错
# self.assertEqual(True, resp.json().get("success"))
# self.assertEqual(10000, resp.json().get("code"))
# self.assertIn("操作成功", resp.json().get("message"))
def common_assert(self, resp, status_code, success, code, message):
self.assertEqual(status_code, resp.status_code)
self.assertEqual(success, resp.json().get("success"))
self.assertEqual(code, resp.json().get("code"))
self.assertIn(message, resp.json().get("message"))
# 调⽤演示
common_assert(self, resp, 200, True, 10000, "操作成功")
common_assert(self, resp, 200, False, 20001, "⽤户名或密码错误")
common_assert(self, resp, 200, False, 99999, "抱歉,系统繁忙,请稍后重试")
导包注意事项;
Tpshop商城参数化
提取每个测试⽤例 使⽤的 测试数据 和 断⾔数据
封装函数,将数据转换为元组列表
参数化步骤:
- 导包 from parameterized import parameterized
- 在 通⽤测试⽅法,上⼀⾏,添加 @parameterized.expand()
- 给 expand() 传⼊ [(),(),()](调⽤ 转换 [{},{},{}] --> [(),(),()] 的函数)
- 修改 通⽤测试⽅法,添加形参,个数、顺序,与 [{},{},{}] 中 { } 内的所有 key 完全⼀⼀对应。
- 在 通⽤测试⽅法内,使⽤形参。
代码实现
from parameterized import parameterized
class TestTpshopLogin(unittest.TestCase):
# 添加类属性
session = None
@classmethod
def setUpClass(cls) -> None:
cls.session = requests.Session()
def setUp(self) -> None:
# 调⽤ ⾃⼰封装的接⼝,获取验证码
TpshopLoginApi.get_verify(self.session)
# 测试 tpshop 登录
@parameterized.expand(read_json_data())
def test_tpshop_login(self, req_body, status_code, status, msg):
resp = TpshopLoginApi.login(self.session, req_body)
common_assert(self, resp, status_code, status, msg)
获取请求头
- 在 common/ 下 创建 get_header.py 文件
- 在 文件内 创建 get_header() 函数,实现 登录成功,获取令牌,拼接成 请求头,返回。
- 在 scripts/ 的测试脚本文件中,添加 setUpClass 方法,调用 get_header() 函数。 将返回值 保存到 类属性上
- 在 使用 请求头的位置,直接从类属性获取
# 在 common/ 下 创建 get_header.py 文件 实现 get_header 函数
import requests
def get_header():
url = "http://ihrm-test.itheima.net/api/sys/login"
data = {"mobile": "13800000002", "password": "123456"}
resp = requests.post(url=url, json=data)
print(resp.json())
# 从 响应体中,获取 data的值
token = resp.json().get("data")
header = {"Content-Type": "application/json",
"Authorization": "Bearer " + token}
return header
----------------------------
# 在 scripts/ 的测试脚本文件中,添加 setUpClass 方法,调用 get_header 函数。 将返回值 保存到 类属性上
from common.get_header import get_header
class TestEmpAdd(unittest.TestCase):
# 类属性
header = None
@classmethod
def setUpClass(cls) -> None:
cls.header = get_header()
----------------------------
# 在 使用 请求头的位置,直接从类属性获取
resp = IhrmEmpCURD.add_emp(self.header, json_data)
提取项目目录
相关知识:
__ file__ : 获取 当前文件的 绝对路径。
BASE_DIR = os.path.dirname(__ file__) : 获取 到 当前文件的 上一级目录。
此行代码,写在 config.py 中, 可以直接获取 项目目录
项目中使用:
- 在 config.py 文件中,添加 获取项目路径 全局变量 BASE_DIR = os.path.dirname(file)
- 修改 common/ 下 read_json_util.py 文件中,读取 json 文件 函数read_json_data(),添加 参数
path_filename - 在 使用 read_json_data()函数 时, 拼接 json 文件路径, 传入到 函数中。
生成测试报告
步骤:
- 创建测试套件实例。 suite
- 添加 测试类
- 创建 HTMLTestReport 类实例。 runner
- runner 调用 run(), 传入 suite
import unittest
from config import BASE_DIR
from scripts.test_emp_add_params import TestEmpAddParams
from scripts.test_ihrm_login_params import TestIhrmLoginParams
from htmltestreport import HTMLTestReport
# 1. 创建测试套件实例。 suite
suite = unittest.TestSuite()
# 2. 添加 测试类, 组装测试用例
suite.addTest(unittest.makeSuite(TestIhrmLoginParams))
suite.addTest(unittest.makeSuite(TestEmpAddParams))
# 3. 创建 HTMLTestReport 类实例。 runner
# runner = HTMLTestReport(BASE_DIR + "/report/ihrm.html") # 绝对路径
runner = HTMLTestReport("./report/ihrm.html", description="描述", title="标题") # 相对路径
# 4. runner 调用 run(), 传入 suite
runner.run(suite)
日志收集
什么是日志
日志也叫 log,通常对应的 xxx.log 的日志文件。文件的作用是记录系统运行过程中,产生的信息。
搜集日志的作用
查看系统运行是否正常。
分析、定位 bug。
日志的级别
logging.DEBUG:调试级别【高】
logging.INFO:信息级别【次高】
logging.WARNING:警告级别【中】
logging.ERROR:错误级别【低】
logging.CRITICAL:严重错误级别【极低】
特性:
日志级别设定后,只有比该级别低的日志会写入日志。
日志代码实现
# 0. 导包
# 1. 创建日志器对象
# 2. 设置日志打印级别
# logging.DEBUG 调试级别
# logging.INFO 信息级别
# logging.WARNING 警告级别
# logging.ERROR 错误级别
# logging.CRITICAL 严重错误级别
# 3. 创建处理器对象
# 创建 输出到控制台 处理器对象
# 创建 输出到日志文件 处理器对象
# 4. 创建日志信息格式
# 5. 将日志信息格式设置给处理器
# 设置给 控制台处理器
# 设置给 日志文件处理器
# 6. 给日志器添加处理器
# 给日志对象 添加 控制台处理器
# 给日志对象 添加 日志文件处理器
# 7. 打印日志
"""
import logging.handlers
import logging
import time
# 1. 创建日志器对象
logger = logging.getLogger()
# 2. 设置日志打印级别
logger.setLevel(logging.DEBUG)
# logging.DEBUG 调试级别
# logging.INFO 信息级别
# logging.WARNING 警告级别
# logging.ERROR 错误级别
# logging.CRITICAL 严重错误级别
# 3.1 创建 输出到控制台 处理器对象
st = logging.StreamHandler()
# 3.2 创建 输出到日志文件 处理器对象
fh = logging.handlers.TimedRotatingFileHandler('a.log', when='midnight', interval=1,
backupCount=3, encoding='utf-8')
# when 字符串,指定日志切分间隔时间的单位。midnight:凌晨:12点。
# interval 是间隔时间单位的个数,指等待多少个 when 后继续进行日志记录
# backupCount 是保留日志文件的个数
# 4. 创建日志信息格式
fmt = "%(asctime)s %(levelname)s [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s"
formatter = logging.Formatter(fmt)
# 5.1 日志信息格式 设置给 控制台处理器
st.setFormatter(formatter)
# 5.2 日志信息格式 设置给 日志文件处理器
fh.setFormatter(formatter)
# 6.1 给日志器对象 添加 控制台处理器
logger.addHandler(st)
# 6.2 给日志器对象 添加 日志文件处理器
logger.addHandler(fh)
# 7. 打印日志
while True:
# logging.debug('我是一个调试级别的日志')
# logging.info('我是一个信息级别的日志')
logging.warning('test log sh-26')
# logging.error('我是一个错误级别的日志')
# logging.critical('我是一个严重错误级别的日志')
time.sleep(1)
日志使用
可修改的位置
使用步骤:
- 调用 init_log_config() 函数,初始化日志信息。
- 指定 日志级别,打印 日志信息。
全量字段校验
概念:校验接⼝返回响应结果的全部字段(更进一步的断言)
校验内容:
字段值
字段名 或 字段类型
校验流程:
定义json语法校验格式
⽐对接口实际响应数据是否符合json校验格式
安装jsonschema:
查验:
pip 查验:pip list 或 pip show jsonschema
pycharm 中 查验:file — settings — 项目名中查看 python解释器列表
JSON Schema⼊⻔
{
“type”: “object”,
“properties”: {
“success”: {“type”: “boolean”},
“code”: {“type”: “integer”},
“message”: {“type”: “string”}
},
“required”: [“success”, “code”, “message”]
}
校验方法
在线工具
http://json-schema-validator.herokuapp.com
https://www.jsonschemavalidator.net
python代码校验
实现步骤:
1 导包 import jsonschema
2 定义 jsonschema格式 数据校验规则
3 调⽤ jsonschema.validate(instance=“json数据”, schema=“jsonshema规则”)
查验校验结果:
校验通过:返回 None
校验失败
schema 规则错误,返回 SchemaError
json 数据错误,返回 ValidationError
# 1. 导包
import jsonschema
# 2. 创建 校验规则
schema = {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"code": {
"type": "int"
},
"message": {
"type": "string"
}
},
"required": ["success", "code", "message"]
}
# 准备待校验数据
data = {
"success": True,
"code": 10000,
"message": "操作成功"
}
# 3. 调用 validate 方法,实现校验
result = jsonschema.validate(instance=data, schema=schema)
print("result =", result)
# None: 代表校验通过
# ValidationError:数据 与 校验规则不符
# SchemaError: 校验规则 语法有误
json schema语法
type关键字
作用:约束数据类型
integer —— 整数
string —— 字符串
object —— 对象
array —— 数组 --> python:list 列表
number —— 整数/⼩数
null —— 空值 --> python:None
boolean —— 布尔值
语法:
{
"type": "数据类型"
}
示例
import jsonschema
# 准备校验规则
schema = {
"type": "object" # 注意 type 和 后面的 类型,都要放到 ”“ 中!
}
# 准备数据
data = {"a": 1, "b": 2}
# 调用函数
res = jsonschema.validate(instance=data, schema=schema)
print(res)
properties关键字
说明:是 type关键字的辅助。用于 type 的值为 object 的场景。
作用:指定 对象中 每个字段的校验规则。 可以嵌套使用。
语法:
{
"type": "object",
"properties":{
"字段名1":{规则},
"字段名2":{规则},
......
}
}
案例
{
"success": true,
"code": 10000,
"message": "操作成功",
"money": 6.66,
"address": null,
"data": {
"name": "tom"
},
"luckyNumber": [6, 8, 9]
}
import jsonschema
# 准备校验规则
schema = {
"type": "object",
"properties": {
"success": {"type": "boolean"},
"code": {"type:": "integer"},
"message": {"type": "string"},
"money": {"type": "number"},
"address": {"type": "null"},
"data": {"type": "object"},
"luckyNumber": {"type": "array"}
}
}
# 准备测试数据
data = {
"success": True,
"code": 10000,
"message": "操作成功",
"money": 6.66,
"address": None,
"data": {
"name": "tom"
},
"luckyNumber": [6, 8, 9]
}
# 调用方法进行校验
res = jsonschema.validate(instance=data, schema=schema)
print(res)
案例2:要求定义JSON对象中包含的所有字段及数据类型
data = {
"success": True,
"code": 10000,
"message": "操作成功",
"money": 6.66,
"address": None,
"data": {
"name": "tom",
"age": 18,
"height": 1.78
},
"luckyNumber": [6, 8, 9]
}
import jsonschema
# 准备校验规则
schema = {
"type": "object",
"properties": {
"success": {"type": "boolean"},
"code": {"type:": "integer"},
"message": {"type": "string"},
"money": {"type": "number"},
"address": {"type": "null"},
"data": {
"type": "object",
"properties": { # 对 data 的对象值,进一步进行校验
"name": {"type": "string"},
"age": {"type": "integer"},
"height": {"type": "number"}
}
},
"luckyNumber": {"type": "array"}
}
}
# 准备测试数据
data = {
"success": True,
"code": 10000,
"message": "操作成功",
"money": 6.66,
"address": None,
"data": {
"name": "tom",
"age": 18,
"height": 1.78
},
"luckyNumber": [6, 8, 9]
}
# 调用方法进行校验
res = jsonschema.validate(instance=data, schema=schema)
print(res)
required关键字
作用:校验对象中必须存在的字段。字段名必须是字符串,且唯⼀
语法:
{
"required": ["字段名1", "字段名2", ...]
}
import jsonschema
# 测试数据
data = {
"success": True,
"code": 10000,
"message": "操作成功",
"data": None,
}
# 校验规则
schema = {
"type": "object",
"required": ["success", "code", "message", "data"]
}
# 调用方法校验
res = jsonschema.validate(instance=data, schema=schema)
print(res)
const关键字
作用:校验字段值是⼀个固定值。
语法:
{
"字段名":{"const": 具体值}
}
import jsonschema
# 测试数据
data = {
"success": True,
"code": 10000,
"message": "操作成功",
"data": None,
}
# 校验规则
schema = {
"type": "object",
"properties": {
"success": {"const": True},
"code": {"const": 10000},
"message": {"const": "操作成功"},
"data": {"const": None}
},
"required": ["success", "code", "message", "data"]
}
# 调用方法校验
res = jsonschema.validate(instance=data, schema=schema)
print(res)
pattern关键字
作用:指定正则表达式,对字符串进行模糊匹配
基础正则举例:
1 包含字符串:hello
2 以字符串开头 ^: ^hello 如:hello,world
3 以字符串结尾 $: hello$ 如:中国,hello
4 匹配[]内任意1个字符[]: [0-9]匹配任意⼀个数字 [a-z]匹任意一个小写字母 [cjfew9823]匹配任意一个
5 匹配指定次数{}: [0-9]{11}匹配11位数字。
匹配 手机号:^[0-9]{11}$
语法:
{
"字段名":{"pattern": "正则表达式"}
}
import jsonschema
# 测试数据
data = {
"message": "!jeklff37294操作成功43289hke",
"mobile": "15900000002"
}
# 校验规则
schema = {
"type": "object",
"properties": {
"message": {"pattern": "操作成功"},
"mobile": {"pattern": "^[0-9]{11}$"}
}
}
# 调用方法校验
res = jsonschema.validate(instance=data, schema=schema)
print(res)
综合案例应用
# 测试数据
import jsonschema
data = {
"success": False,
"code": 10000,
"message": "xxx登录成功",
"data": {
"age": 20,
"name": "lily"
}
}
# 校验规则
schema = {
"type": "object",
"properties": {
"success": {"type": "boolean"},
"code": {"type": "integer"},
"message": {"pattern": "登录成功$"},
"data": {
"type": "object",
"properties": {
"name": {"const": "lily"},
"age": {"const": 20}
},
"required": ["name", "age"]
}
},
"required": ["success", "code", "message", "data"]
}
# 调用测试方法
res = jsonschema.validate(instance=data, schema=schema)
print(res)
案例练习
传智健康管理系统,是一款应用于健康管理机构的业务系统。采用可视化界面管理,提高健康管理师工作效
率,加强与患者间的互动
前端:http://mobile-health-test.itheima.net
后端:http://manager-health-test.itheima.net
Dubbo接口测试
RPC
远程过程调用(Remote Procedure Call):像调用本地方法一样,调用远程方法。
常见的RPC框架有 Dubbo、Thrift、grpc
Dubbo
Dubbo是一款高性能、轻量级、基于Java的开源RPC框架(最早由阿里开源,2018年贡献给了Apache组织)
Dubbo接口的作用:远程调用 java 写的方法。 需要传参、获取返回值。
查阅API文档
从中获取哪些信息?
服务名
方法名
参数类型、返回值类型
java中 方法定义语法结构:
返回值类型 方法名(数据类型 形参1,数据类型 形参2,…)
void:代表没有返回值、没有参数。
Telnet工具远程调用
启用telnet
telnet远程连接服务
连接语法:连接语法:telnet IP 端口号
telnet调用服务接口
命令格式:invoke 服务名.方法名(实参)
示例:invoke MemberService.findByTelephone(“13020210001”)
python借助dubbo远程调用
Dubboclient,封装了 telnetlib 库。 telnetlib 是 python 内置模块,可实现远程调用 Dubbo 接口
安装dubboclient
pip install dubboclient
查验:
在 pip 中:pip list 或 pip show dubboclient
在 pycharm中:file - settings - 项目名下的 python 解释器列表
实现步骤:
1. 导包 from dubboclient import DubboClient
2. 创建 DubboClient类实例,指定 IP 和 port
3. 使用 实例调用 invoke() 方法。 传入 :服务名、方法名、实参(方法使用)。获取响应结果
4. 打印响应结果
会员服务(入门)
案例1
根据手机号,查询会员信息(传递 普通参数)
dubbo> ls -l MemberService
com.itheima.pojo.Member findByTelephone(java.lang.String)
接口定义:Member findByTelephone(String telephone)
参数:
字符串格式手机号。唯一
返回值:
成功:返回 会员的信息内容。string类型 包裹的 字典数据。
失败:返回 null。string类型
实现代码:
# 1. 导包 from dubboclient import DubboClient
from dubboclient import DubboClient
# 2. 创建 DubboClient类实例,指定 IP 和 port
dubboclt = DubboClient("211.103.136.244", 6502)
# 3. 使用 实例调用 invoke() 方法。 传入 :服务名、方法名、实参(方法使用)。获取响应结果
resp = dubboclt.invoke("MemberService", "findByTelephone", "13020210001")
# 4. 打印响应结果
print("响应结果 =", resp)
print("type(resp) =", type(resp))
添加会员(传递 对象参数)
dubbo> ls -l MemberService
void add(com.itheima.pojo.Member)
接口定义:void add(Member member)
参数:
1. 自定义类 做 参数,根据接口文档,组织 “字典” 格式数据传参
2. 给字典增加 键k:”class“ ,值v:指明 类 对应的 完整 包名和类名
如:"class”:"com.itheima.pojo.Member"
ls -l MemberService 可以查看完整包名和类名。
区分自定义类: 包名不以“java.”开头。一般采用:com.公司名.项目名.类名
返回值:
成功:返回 null
失败:返回 Failed
实现代码:
# 1. 导包
from dubboclient import DubboClient
# 2. 创建 dubboclient 实例
dubboclt = DubboClient("211.103.136.244", 6502)
# 准备 add 方法,所需要的数据
info = {"id": 911, "name": "杜甫", "phoneNumber": "13048379884"}
# 如果 class 已经存在,覆盖原有class值; 如果不存在 class,新增一组 元素到 字典中。
info["class"] = "com.itheima.pojo.Member"
# 3. 调用 invoke 传 服务名、方法名、实参。得响应结果
resp = dubboclt.invoke("MemberService", "add", info)
# 4. 打印
print("响应结果 =", resp)
print("type(resp) =", type(resp))
根据日期统计会员数(传递 字符串列表)
dubbo> ls -l MemberService
java.util.List findMemberCountByMonths(java.util.List)
接口定义:List<Integer> findMemberCountByMonths(List<String> months)
参数:
1. 字符串列表。用字符串表示年、月,用“.”衔接
如:["2021.3", "2021.9"]
返回值:
成功:返回列表,对应参数设置的月份的会员数。
失败:Failed
实现代码
# 1. 导包
from dubboclient import DubboClient
# 2. 创建 dubboclient 实例
dubboclt = DubboClient("211.103.136.244", 6502)
# 3. 用实例 调用invoke() ,传入 服务名、方法名、实参。 得响应结果
months = ["2021-7"]
resp = dubboclt.invoke("MemberService", "findMemberCountByMonths", months)
# 4. 查看响应结果
print("响应结果 =", resp)
print("type(resp) =", type(resp))
其他模块
添加预约设置
dubbo> ls -l OrderSettingService
void add(java.util.List)
接口定义:void add(List<OrderSetting> list)
参数:
1. 字典列表。字典有 orderDate 和 number 两个字段。
如:[{"orderDate":"2021-09-20 16:45:12","number":20}]
2. 日期格式:"2021-09-20 16:45:12",必须包含时分秒,否则失败。
返回值:
成功:null
失败:Failed
实现代码
# 1. 导包
from dubboclient import DubboClient
# 2. 创建 dubboclient 实例
dubboclt = DubboClient("211.103.136.244", 6502)
# 准备 add 方法,所需要的数据
info = [{"orderDate": "2021-05-18 18:89:02", "number": 346}]
# 3. 调用 invoke 传 服务名、方法名、实参。得响应结果
resp = dubboclt.invoke("OrderSettingService", "add", info)
# 4. 打印
print("响应结果 =", resp)
print("type(resp) =", type(resp))
按月统计预约设置信息
dubbo> ls -l OrderSettingService
java.util.List getOrderSettingByMonth(java.lang.String)
接口定义:List getOrderSettingByMonth(String date)
参数:
字符串,如:"2021-09"
返回值:
成功:返回字符串类型数据,字符串内容为列表
失败:Failed
# 1. 导包
from dubboclient import DubboClient
# 2. 创建 dubboclient 实例
dubboclt = DubboClient("211.103.136.244", 6502)
# 月份
moths = "2021.02"
# 3. 调用 invoke 传 服务名、方法名、实参。得响应结果
resp = dubboclt.invoke("OrderSettingService", "getOrderSettingByMonth", moths)
# 4. 打印
print("响应结果 =", resp)
print("type(resp) =", type(resp))
根据日期修改预约设置数量
dubbo> ls -l OrderSettingService
void editNumberByDate(com.itheima.pojo.OrderSetting)
接口定义:void editNumberByDate(OrderSetting orderSetting)
参数:
1. 自定义类,用 字典 根据接口文档组织数据
2. 需要使用 class 指定参数对象的类型
如:{"orderDate":"2021-10-13 21:04:33","number":15,
"class":"com.itheima.pojo.OrderSetting"}
3. 日期格式为:"2021-10-13 21:04:33",必须包含时分秒
返回值:
成功:null
失败:Failed
# 1. 导包
from dubboclient import DubboClient
# 2. 创建 dubboclient 实例
dubboclt = DubboClient("211.103.136.244", 6502)
# 日期 和 设置数据
date = {"orderDate": "2021-06-15 16:99:77", "number": 120}
date["class"] = "com.itheima.pojo.OrderSetting"
# 3. 调用 invoke 传 服务名、方法名、实参。得响应结果
resp = dubboclt.invoke("OrderSettingService", "editNumberByDate", date)
# 4. 打印
print("响应结果 =", resp)
print("type(resp) =", type(resp))
根据用户名查询用户信息
dubbo> ls -l UserService
com.itheima.pojo.User findByUsername(java.lang.String)
接口定义:User findByUsername(String username)
参数:字符串类型,如:'admin'
返回值:
用户存在:返回用户信息
用户不存在:返回 null
# 1. 导包
from dubboclient import DubboClient
# 2. 创建 dubboclient 实例
dubboclt = DubboClient("211.103.136.244", 6502)
# 管理用户名
name = "admin"
# 3. 调用 invoke 传 服务名、方法名、实参。得响应结果
resp = dubboclt.invoke("UserService", "findByUsername", name)
# 4. 打印
print("响应结果 =", resp)
print("type(resp) =", type(resp))
现有问题:
远程调用的 7个 dubbo接口 存在的问题:
- 代码有 大量冗余
- 测试接口时,除了要给 测试数据之外,还需要 指定 服务名、方法名
- 传参时,除了要考虑测试数据外,还要分析是否要添加 class 字段 及 对应数据。
- 返回的数据类型统一为 string(不具体)
封装目标
5. 只关心:测试数据、响应结果
6. 返回的结果 分别为 不同的 具体类型。
Dubbo接口自动化测试框架
核心模块
基础服务对象封装
from dubboclient import DubboClient
class BaseService(object):
def __init__(self):
self.dubbo_client = DubboClient("211.103.136.244", 6502)
服务对象封装
会员服务封装
"""
类名:MemberService,继承于 BaseService
实例属性:
服务名称:service_name,赋值为 'MemberService'
实例方法:
def __init__(self):
# 先调父类__init__(),再添加实例属性 service_name
def find_by_telephone(self, telephone):
# 功能:根据手机号查询会员信息
# :param telephone: 手机号
# :return: 1. 会员存在,返回会员信息 2. 会员不存在,返回None
def find_member_count_by_months(self, data_list):
# 功能:根据日期统计会员数
# :param date_list: 日期列表,格式如:["2021.7"]
# :return: 返回列表,列表元素为对应月份的会员数,如:[10]
def add(self, info): 添加会员
# 功能:添加会员
# :param info: 会员信息的字典格式数据,参考接口文档填入字段数据,手机号需要唯一
# 如:{"fileNumber":"D0001", "name":"李白", "phoneNumber":"13020210002"}
# :return: 添加成功返回True, 添加失败返回False
验证结果:
# 1. 实例化对象
# 2. 通过实例对象调用实例方法
# 2.1 根据手机号查询会员信息
# 2.2 根据日期统计会员数
# 2.3 添加会员
"""
import json
from day02.base_service import BaseService
# 将 会员服务 封装成 会员服务类
class MemberService(BaseService):
def __init__(self):
super().__init__() # 调用父类 init 方法
self.service_name = "MemberService"
def find_by_telephone(self, tel):
resp = self.dubbo_client.invoke(self.service_name, "findByTelephone", tel)
if resp == "null":
return None
else:
# 作用:将 string类型的 数据,还原回成 字典 或 列表 数据。
return json.loads(resp)
def find_member_count_by_months(self, months):
resp = self.dubbo_client.invoke(self.service_name, "findMemberCountByMonths", months)
# 作用:将 string类型的 数据,还原回成 字典 或 列表 数据。
return json.loads(resp)
def add(self, info):
"""
:param info: 代表 用户 传入的 测试数据,没有 class 元素
:return:
"""
# 如果 class 已经存在,覆盖原有class值; 如果不存在 class,新增一组 元素到 字典中。
info["class"] = "com.itheima.pojo.Member"
# 3. 调用 invoke 传 服务名、方法名、实参。得响应结果
resp = self.dubbo_client.invoke(self.service_name, "add", info)
if resp == "null":
return True
else:
return False
if __name__ == '__main__':
ms = MemberService()
resp = ms.find_by_telephone("13020210001")
print("响应结果 =", resp)
print("type(resp) =", type(resp))
print("=" * 66)
months = ["2021-6"]
ms = MemberService()
resp = ms.find_member_count_by_months(months)
print("响应结果 =", resp)
print("type(resp) =", type(resp))
print("&" * 66)
# 准备 add 方法,所需要的数据
info = {"id": 911, "name": "杜甫", "phoneNumber": "13048379041"}
ms = MemberService()
resp = ms.add(info)
print("响应结果 =", resp)
print("type(resp) =", type(resp))
预约设置服务封装
"""
类名:OrderSettingService,继承于 BaseService
实例属性:
服务名称:service_name,赋值为 'OrderSettingService'
实例方法:
def __init__(self):
# 先调父类__init__(),再添加实例属性 service_name
def add(self, date_list):
# 功能:添加预约设置
# :param date_list:
# 1. 日期列表,如:[{"orderDate":"2021-09-20 16:45:12","number":20}]
# 2. 日期格式为:"2021-09-20 16:45:12",必须包括时分秒
# :return: 设置成功返回True, 设置失败返回False
def get_order_setting_by_month(self, date):
# 功能:按月统计预约设置信息
# :param date: 日期,如:"2021-08"
# :return: 列表,指定月份的预约信息
def edit_number_by_date(self, info): 根据日期修改预约设置数量
# 功能:根据日期修改预约设置数量
# :param info:
# 1. 预约设置的字典格式数据,参考接口文档填入字段数据
# 2. 如:{"orderDate":"2021-09-19 17:45:12","number":15}
# 3. 日期格式为:"2021-09-19 17:45:12",必须包括时分秒
# 4. 添加 "class":"com.itheima.pojo.OrderSetting"
# :return: 修改成功返回 True, 修改失败返回 False
验证结果:
# 1. 实例化对象
# 2. 通过实例对象调用实例方法
# 2.1 添加预约设置
# 2.2 按月统计预约设置信息
# 2.3 根据日期修改预约设置数量
"""
import json
from day02.base_service import BaseService
# 封装 预约设置服务类
class OrderSettingService(BaseService):
def __init__(self):
super().__init__()
self.service_name = "OrderSettingService"
def add(self, date_list):
# 功能:添加预约设置
# :param date_list:
# 1. 日期列表,如:[{"orderDate":"2021-09-20 16:45:12","number":20}]
# 2. 日期格式为:"2021-09-20 16:45:12",必须包括时分秒
# :return: 设置成功返回 True, 设置失败返回 False
resp = self.dubbo_client.invoke(self.service_name, "add", date_list)
if resp == "Failed":
return False
else:
return True
def get_order_setting_by_month(self, month):
# 功能:按月统计预约设置信息
# :param months: 日期,如:"2021-08"
# :return: 列表,指定月份的预约信息
resp = self.dubbo_client.invoke(self.service_name, "getOrderSettingByMonth", month)
if resp == "Failed":
return None
else:
return json.loads(resp)
def edit_number_by_date(self, date):
# 功能:根据日期修改预约设置数量
# :param info:
# 1. 预约设置的字典格式数据,参考接口文档填入字段数据
# 2. 如:{"orderDate":"2021-09-19 17:45:12","number":15}
# 3. 日期格式为:"2021-09-19 17:45:12",必须包括时分秒
# 4. 添加 "class":"com.itheima.pojo.OrderSetting"
# :return: 修改成功返回 True, 修改失败返回 False
date["class"] = "com.itheima.pojo.OrderSetting"
# 3. 调用 invoke 传 服务名、方法名、实参。得响应结果
resp = self.dubbo_client.invoke(self.service_name, "editNumberByDate", date)
if resp == "Failed":
return False
else:
return True
if __name__ == '__main__':
oss = OrderSettingService()
# 准备 add 方法,所需要的数据
info = [{"orderDate": "2021-05-18", "number": 346}]
resp = oss.add(info)
print("响应结果 =", resp)
print("type(resp) =", type(resp))
print("============== 按月统计预约设置信息 ===========")
oss = OrderSettingService()
# 月份
months = "2021.02"
resp = oss.get_order_setting_by_month(months)
print("响应结果 =", resp)
print("type(resp) =", type(resp))
print("============== 根据日期修改预约设置数量 ===========")
# 日期 和 设置数据
date = {"orderDate": "2021-06-15 16:99:77", "number": 120}
oss = OrderSettingService()
resp = oss.edit_number_by_date(date)
print("响应结果 =", resp)
print("type(resp) =", type(resp))
用户服务封装
"""
类名:UserService,继承于BaseService
实例属性:
服务名称:service_name,赋值为'UserService'
实例方法:
def __init__(self):
# 先调父类__init__(),再添加实例属性 service_name
def find_by_username(self, username):
# 功能:根据用户名查询用户信息
# :param username: 用户名
# :return: 1. 如果用户存在,返回用户信息 2. 如果不存在,返回 None
验证结果:
# 1. 实例化对象
# 2. 通过实例对象调用实例方法
"""
import json
from day02.base_service import BaseService
# 封装 用户服务类
class UserService(BaseService):
def __init__(self):
super().__init__()
def find_by_user_name(self, name):
# 3. 调用 invoke 传 服务名、方法名、实参。得响应结果
resp = self.dubbo_client.invoke("UserService", "findByUsername", name)
if resp == "null":
return None
else:
return json.loads(resp)
if __name__ == '__main__':
# 管理员用户名
name = "李白"
us = UserService()
resp = us.find_by_user_name(name)
print("响应结果 =", resp)
print("type(resp) =", type(resp))
测试用例对象封装
import unittest
# 借助 unittest 框架,封装测试类,从 TestCase 继承
from day02.py02_会员服务类封装设计 import MemberService
class TestFindByTelephone(unittest.TestCase):
ms = None
@classmethod
def setUpClass(cls) -> None:
# 创建MemberService实例
cls.ms = MemberService()
def test01_tel_exists(self):
tel = "13020210001"
resp = self.ms.find_by_telephone(tel)
print("手机号存在 =", resp)
self.assertEqual("13020210001", resp.get("phoneNumber"))
def test02_tel_not_exists(self):
tel = "13020218973"
resp = self.ms.find_by_telephone(tel)
print("手机号不存在 =", resp)
self.assertEqual(None, resp)
def test03_tel_has_special_char(self):
tel = "1302021abc#"
resp = self.ms.find_by_telephone(tel)
print("手机号含有字母特殊字符 =", resp)
self.assertEqual(None, resp)
参数化
- 导包 from parameterized import parameterized
- 在 通用测试方法上一行,@parameterized.expand()
- 给 expand() 传入 [(),(),()] 类型的数据。
- 修改 通用测试方法,添加形参,个数、顺序与 () 数据一致。
- 在 通用测试方法 使用形参
import unittest
from day02.py02_会员服务类封装设计 import MemberService
from parameterized import parameterized
# 借助 unittest 框架,封装测试类,从 TestCase 继承
class TestMemberService(unittest.TestCase):
ms = None
@classmethod
def setUpClass(cls) -> None:
cls.ms = MemberService() # 创建MemberService实例
# 通用测试方法(参数化)
@parameterized.expand([("13020210001", "13020210001"),
("13020218973", None),
("1302021abc#", None)])
def test_findByTelephone(self, tel, except_data):
# print("tel =", tel, "except_data =", except_data)
resp = self.ms.find_by_telephone(tel)
if resp is None:
self.assertEqual(None, resp)
else:
self.assertEqual(except_data, resp.get("phoneNumber"))
@parameterized.expand([(["2021.5"], [3]),
(["2017.4"], [0])])
def test_findMemberCountByMonths(self, month, except_data):
resp = self.ms.find_member_count_by_months(month)
print("============ resp =============", resp)
self.assertEqual(except_data, resp)
# def test01_tel_exists(self):
# tel = "13020210001"
# resp = self.ms.find_by_telephone(tel)
# print("手机号存在 =", resp)
#
# self.assertEqual("13020210001", resp.get("phoneNumber"))
#
# def test02_tel_not_exists(self):
# tel = "13020218973"
# resp = self.ms.find_by_telephone(tel)
# print("手机号不存在 =", resp)
#
# self.assertEqual(None, resp)
#
# def test03_tel_has_special_char(self):
# tel = "1302021abc#"
# resp = self.ms.find_by_telephone(tel)
# print("手机号含有字母特殊字符 =", resp)
#
# self.assertEqual(None, resp)
接口自动化框架封装
测试报告
# 导包
import unittest
from htmltestreport import HTMLTestReport
# 创建 suite 实例
from scripts.test_member_service import TestMemberService
suite = unittest.TestSuite()
# 添加测试用例
suite.addTest(unittest.makeSuite(TestMemberService))
# 创建 HTMLTestReport 类对象
runner = HTMLTestReport("./report/传智健康测试报告.html", description="描述", title="标题")
# 调用 run() 传入 suite
runner.run(suite)
SeaHaloa: 文章很赞,博主请问下,安享金融后台管理的admin的密码是不是改了? 123456错误
weixin_51197295: sql数据的连接有吗
CSDN-Ada助手: 恭喜你这篇博客进入【CSDN每天值得看】榜单,全部的排名请看 https://bbs.csdn.net/topics/616897427。
CSDN-Ada助手: 恭喜你,获得了 2023 博客之星评选的入围资格,请看这个帖子 (https://bbs.csdn.net/topics/616395297?utm_source=blogger_star_comment)。 请在这里提供反馈: https://blogdev.blog.csdn.net/article/details/129986459?utm_source=blogger_star_comment。
CSDN-Ada助手: 恭喜你这篇博客进入【CSDN每天值得看】榜单,全部的排名请看 https://bbs.csdn.net/topics/616395337。