接口测试基础知识 接口测试概念 接口测试是测试系统组件间接口的一种测试。 接口测试主要用于检测外部系统与系统之 间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换,传递和控制管理过 程,以及系统间的相互逻辑依赖关系等。
接口测试目的
核心:保证系统的稳定
手段:持续集成
目的:提高测试效率,提升用户体验,降低产品研发成本
接口测试一般流程
列出需求
安排资源,编写接口用例 -> 用例评审
编写接口测试代码 -> 代码评审codeReview
执行接口测试
接口测试关注点
功能:功能实现,实现与设计一致, 接口通过性测试
健壮性: 边界值,容错性
性能: 并发及压测
稳定性: 长期运行的稳定性
安全性: SQL注入, session依赖, 数字签名, http接口的安全性
常见接口种类
Http/Https接口: 通过http/https协议传送接口数据(通常按字符串/二进制传输), 如常见的网页表单, https安全性更好
RESTful Api: REST表述性状态传递. 一种设计风格,基于http/https协议, 把一切接口视为资源, 接口要分版本,在统一的域名下管理, 不同的方法(get/post..)做不同的事,通常请求及响应使用json格式
Web Service: SOAP简单面向对象协议, 基于http实现的一种RPC方案.接口返回一些对象,可以直接通过操作对象,实现我们需要的业务处理.使用xml格式传输数据
RPC接口: RPC为远程方法调用, 有不同的实现方案,基于TCP/Http协议的都有. RPC可以想我们本地导入和调用对象一样使用. Dubbo接口也是一种RPC接口.
常见接口数据类型
请求数据类型(Content-Type):
application/x-www-form-urlencoded: 常规只有文本的网页表单
application/json: RESTful Api常用格式, 结构清晰, 含有多层嵌套
multipart/form-data: 既有文本,又有上传文件或富文本框的混合数据表单
text/xml: xml格式, RPC接口常用格式
响应数据类型
string/html: 返回字符串或网页源码
json: RESTful Api常用响应格式, 结构清晰
xml: RPC接口常用格式
常见接口安全验证方式
Auth_1.0/Auth_2.0: 通用接口授权方式
Session依赖: 需要登录之后才能进行接口操作
Token验证: 先要使用自己的appid/appsecret通过获取token接口验证身份获取一个token(令牌,有一定有效期), 然后带着token访问接口
数字签名: 将原本的参数按一定规则进行组合,配合时间戳或appsecret, 通过加密算法生成一个签名sign, 携带签名进行接口请求
常见接口请求方法(RESTful规范)
GET: 获取资源
POST: 修改资源
PUT: 上传资源
DELETE: 删除资源
HEAD: 只请求页面首部
PATCH: 补丁
OPTIONS: 运行客户端查看服务器性能 ……
常见状态码
200系: 成功
200 OK - [GET]:获取资源成功
201 CREATED - [POST/PUT/PATCH]:创建/修改成功
202 Accepted - [*]:任务接受
204 NO CONTENT - [DELETE]:删除成功
300系: 重定向
301 Moved Permanently: 永久重定向
302 Found: 临时重定向
400: 资源错误
400 INVALID REQUEST - [POST/PUT/PATCH]:用户请求错误
401 Unauthorized - [*]:没有权限(鉴权失败, 接口层)
403 Forbidden - [*] 资源禁止访问(服务器层,没有访问权限)
404 NOT FOUND - [*]:资源不存在
405 Method Not Allowd: 访问的方法不允许, 如用POST访问只支持GET请求的接口
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)
410 Gone -[GET]:资源被永久删除
422 Unprocesable entity - [POST/PUT/PATCH] 当创建对象时,发生验证错误
500系: 服务器内部错误(接口崩溃或有Bug)
500 INTERNAL SERVER ERROR - [*]:服务器发生错误
接口业务类型
返回数据型接口: 只从数据库读取数据
业务操作型接口: 需要写数据库(接口测试需要要涉及参数化或环境清理)
接口概念 接口又称API(Application Programming Interface,应用程序编程接口),是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力
简单概括为以下3点:
程序代码(函数方法)
屏蔽实现细节
可以被访问/调用来获取信息或实现某些功能(提供访问地址,定义了访问规则)
接口自述(通俗的来说):
首先我有一些功能(功能函数)
你不用关心我怎么实现的(屏蔽细节)
我会给你一个我的地址(接口地址)
你按照地址找到我,按照我规定的格式(请求类型)告诉我所需要的信息(参数)就行
我会给你个反馈(响应信息)
常见接口类型 HTTP接口:通过HTTP协议传输的接口,可以传输文本表单数据,也可以传输Json类型的对象数据或xml类型的数据
RPC: 远程方法调用,随着分布式系统的出现,当你需要调用部署到其他服务器上的方法时,需要用到RPC。RPC只是提出了这样一个问题,有很多种解决方案,比如WebService(基于SOAP协议), REST(基于HTTP协议)。
SOAP: 简单面向对象协议,基于HTTP,使用xml作为默认传输格式
Web Service: 基于SOAP协议的一种RPC实现方案。相比传统的HTTP接口只传输文本请求和文本相应,通过Web Service可以直接拿到远程的一个对象,并能够直接调用该对象的属性和方法,比HTTP更高级。
REST/RESTful API: REST,表述性状态转移。一种HTTP接口的设计风格,将一切接口视为资源,要求接口路径同意管理,分版本管理,规定了GET/POST等请求以及HTTP状态码的使用规范,默认使用JSON格式传输。RESTful API即满足REST风格即设计规范的API接口
什么是接口测试? 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个 子系统之间的交互点。测试的重点是要检查数据的交换、传递和控制管理过程,以及系统间的相互逻辑依赖关系等。
为什么要做接口测试?
接口测试介于单元测试与系统测试之间,单元测试一般由开发完成(不要相信开发)
接口是各种系统功能的基础,一旦接口出现问题可能会引起许多系统功能的问题并且不容易定位
开展接口测试可以及早发现问题,有效降低测试成本
接口一般较UI相对稳定,利于进行自动化和持续集成
接口测试都测什么? 接口测试一般有以下岗位实施:
手工测试岗:先提测接口再提出功能,兼做接口自动化
服务端测试岗:梳理代码,审核接口实现逻辑是否与业务设计一致,技术实现逻辑的合理性,异常流测试,接口压测及安全性测试
测试开发岗:专职做接口(或UI)的自动化用例开发,测试工具开发
怎样掌握接口测试?
了解OSI网络模型,TCP/UDP协议,掌握HTTP/HTTPS协议,了解RPC, Web Service及REST,理解Session和Cookie
掌握常用的接口测试工具如curl命令,Postman,Jmeter,LR,SoupUI,AB等
掌握基本的抓包工具如Chrome开发者工具,Fiddler,Charles,Wireshark,tcpdumps等
掌握一门编程语言Python或Java
了解Nginx, Apache, Tomcat等服务器中间件
掌握数据库基本查询命令,及一些NoSQL(如Redis)操作,用于检查响应结果
掌握基本的Linux日子查询和筛选命令
接口测试重难点
动态变量参数化
接口依赖及中间变量问题
异步接口结果验证问题
相应参数及嵌套很多的验证问题
接口测试框架的稳定性问题
资源清理问题
多接口场景测试
网络基础知识:IP,域名, DNS及端口 IP地址 IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址
查看IP命令
Windows: ipconfig
Linux: ifconfig
Python练习:检查字符串是否ip 1 2 3 4 5 6 7 8 9 10 11 12 13 14 def is_ip (ip ): num_list = ip.split("." ) for num in num_list: if not num.isdigit() or not 0 <= int (num) <=255 : return False return True print (is_ip("101.1.0.201" ))def check_ipv4 (str ): ip = str .strip().split("." ) return False if len (ip) != 4 or False in map (lambda x:True if x.isdigit() and 0 <= int (x) <= 255 else False , ip) else True
端口 “端口”是英文port的意译,可以认为是设备与外界通讯交流的出口。 如果把IP地址比作一间房子,端口就是出入这间房子的门。一个IP地址的端口可以有65536(即:2^16)个
端口类型
公认端口:从0到1023,紧密绑定于一些服务
注册端口:人1024到49151,许多服务绑定这些端口,这些端口同样用于许多其它目的。
动态或私有端口:从49152到65535。理论上,不应为服务分配这此端口。实际上,机器通常从1024起分配动态端口。
常见软件默认端口
Apache/Nginx(HTTP服务): 80
Tomcat: 8080
Oracle: 1521
MySQL: 3306
SQL Server: 1433
PostgreSQL: 5432
MongoDB: 27017
Redis: 6379
Memcached: 11211
查看端口命令
Windows: netstat -ano
Linux: netstat -ntlp
解决端口占用问题
Windows: netstat -ano | findstr “8080”,找到占用端口的程序的PID -> 打开任务管理器 -> 设置显示PID -> 找到并结束对于程序
Linux: netstat -ntlp | grep “8080”, 找到对应的程序 -> ps -ef | grep “程序名” 找到对于的pid -> kill 相应的id
域名及DNS 由于IP地址不容易记忆,为IP地址赋予了一个利于记忆的别名,称为域名
如何查看域名所对于的ip?
DNS DNS即域名解析系统,域名和IP地址相互映射的一个分布式数据库,提供域名转到对应ip的服务
网络基础知识:OSI七层模型及TCP协议 OSI七层模型 OSI即开放系统互连参考模型,一种网络架构,分为7层
上三层—应用层,控制软件方面
应用层:文件传输,电子邮件,文件服务,虚拟终端 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
表示层:数据格式化,代码转换,数据加密
会话层:解除或建立与别的接点的联系(会话)
下四层—数据流层,用来管理硬件
传输层:提供端对端的接口 TCP,UDP
网络层:为数据包选择路由 IP,ICMP,RIP,OSPF,BGP,IGMP
数据链路层 传输有地址的帧以及错误检测功能 SLIP,CSLIP,PPP,ARP,RARP,MTU
物理层 以二进制数据形式在物理媒体上传输数据 ISO2110,IEEE802,IEEE802.2
OSI七层模型及各层协议 TCP及UDP协议 TCP和UDP都是传输层的协议
TCP和UDP的区别
无链接
UDP使用尽最大努力交付,不保证可靠性
UDP是面向报文的,UDP对应用层交付下来的报文,既不合并,也不拆分,而是保留这些报文的边界。应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文
UDP没有拥塞控制
UDP支持一对一、一对多、多对一和多对多的交互通信
UDP的首部开销小,只有8字节
TCP是面向连接的
每条TCP连接只能用于两个断点,一对一
TCP提供可靠交付的服务:连接传输数据、无差错、不丢失、不重复、并且按序到达
TCP提供全双工通信
面向字节流。TCP根据对方给出的窗口和当前网络拥塞的程度来决定一个报文应该包含多少个字节
HTTP协议 HTTP:超文本传输协议,是用于从WWW服务器传输超文本到本地浏览器的传输协议。 HTTP协议是一种无状态协议,主要包含请求和相应两大部分:
请求 请求是我们发送给接口的数据对象,包含接口地址(URL),请求方法,参数,请求头(Headers), Cookies, 数据等
GET请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET https://www.sojson.com/open /api/weather/json.shtml?city=%E5%8C%97 %E4%BA%AC HTTP/1.1 Host: www.sojson.com Connection: keep-alive Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0 ; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0 .3440 .106 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9 ,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 ,en-US;q=0.8 ,en;q=0.7 Cookie: __cfduid=dccd65c484a7657b468327b66023fefc41534934250; yjs_id=59d1c42afa817b578b4b562d1f72651f; ctrl_time=1 ''' 第1行: 请求方法 接口地址 HTTP协议版本 第2-N行:请求headers(如果有Cookie,最后一行为Cookie) 空一行 请求数据(POST等方法使用,此处为空) '''
POST请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 POST http://openapi.tuling123.com/openapi/api/v2 HTTP/1.1 Content-Type : application/json cache-control: no-cache Postman-Token: 1a39439e-61c8-4e59 -82a1-736a362c5962 User-Agent: PostmanRuntime/7.2 .0 Accept: */* Host: openapi.tuling123.com accept-encoding: gzip, deflate content-length: 468 Connection: keep-alive { "reqType" :0 , "perception" : { "inputText" : { "text" : "附近的酒店" }, "inputImage" : { "url" : "imageUrl" }, "selfInfo" : { "location" : { "city" : "北京" , "province" : "北京" , "street" : "信息路" } } }, "userInfo" : { "apiKey" : "ec961279f453459b9248f0aeb6600bbe" , "userId" : "206379" } }
URL URL:统一资源定位符,接口的访问地址(包含服务器地址+接口地址)
1 2 3 4 5 6 7 8 9 ''' URL编码是一种浏览器用来打包请求参数及表单参数的格式, 参数和参数之间使用&分割,非ASCII码使用%加16进制编码替换 如:https://www.sojson.com/open/api/weather/json.shtml?city=北京 编码后为:https://www.sojson.com/open/api/weather/json.shtml?city=%E5%8C%97%E4%BA%AC '''
请求方法
序号
方法
描述
1
GET
请求指定的页面信息,并返回实体主体
2
POST
向指定资源提交数据进行处理请求(例如提交表单或者上传文件)数据被包含在请求体中
3
HEAD
类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
4
PUT
从客户端向服务器传送的数据取代指定的文档的内容
5
DELETE
请求服务器删除指定的页面
6
CONNECT
预留给能够将连接改为管道方式的代理服务器
7
OPTIONS
允许客户端查看服务器的性能
8
TRACE
回显服务器收到的请求,主要用于测试或诊断
GET请求和POST请求的区别
GET请求:
GET请求可被缓存
GET请求保留在浏览器历史记录中
GET请求可被收藏为书签
GET请求不应在处理敏感数据时使用
GET请求有长度限制
GET请求只应当用于取回数据
POST请求:
POST请求不会被缓存
POST请求不会保留在浏览器历史记录中
POST不能被收藏为书签
POST请求对数据长度没有要求
请求参数(URL参数) 如:https://www.sojson.com/open/api/weather/json.shtml?city=北京
中的city=北京,向接口传递一个参数“city”,参数值为“北京”
不同的参数之间用&隔开,非ASCII码参数会自动url encode
请求数据(又称为Request Body 或 Data) 请求数据类型(Content-Type)(重点)
application/x-www-form-urlencoded: 网页表单格式(默认)
application/json:REST接口常用格式
text/xml:xml格式,RPC接口,Dubbo接口常用格式
test/html: html格式
multipart/form-data: 混合表单,支持上传图片
数据编码
ASCII码: 单字节,美国信息交换标准码, 包含数字,字母,英文标点及一些控制字符
ISO-8859-1:又称Latin1,单字节,向下兼容ASCII,用于支持部分于欧洲使用的语言
ANSI编码:单字节表示英文,双字节表示汉字,对ASCII的扩展,不同的国家和地区制定了不同的标准,中文中的GBK,GB2312属于ANSI编码
Unicode编码: 采用二个字节编码(英文和中文的字符都以双字节存放),与ANSI码不兼容
UTF-8:是目前互联网上使用最广泛的一种Unicode 编码方式,又称万国码
Base64: 一种用64个字符来表示任意二进制数据的方法。
Base64编码的作用:由于某些系统中只能使用ASCII字符。Base64就是用来将非ASCII字符的数据转换成ASCII字符的一种方法。
而且base64特别适合在http,mime协议下快速传输数据。
指定请求数据编码(解决中文乱码): 请求Headers设置Content-Type: application/json; charset=utf-8
响应(Response) 接口返回的信息,包含HTTP状态码,响应头和相应信息
1 2 3 4 5 6 HTTP/1.1 200 OK Date: Thu, 23 Aug 2018 06:32 :26 GMT Transfer-Encoding: chunked Connection: keep-alive {"intent" :{"actionName" :"" ,"code" :10005 ,"intentName" :"" ,"parameters" :{"lon" :"" ,"checkout_date" :"2018-08-25" ,"star" :"0" ,"city" :"北京" ,"days" :"1" ,"order" :"" ,"price_range" :"" ,"nearby_place" :"酒店" ,"brand" :"" ,"checkin_date" :"2018-08-24" ,"place" :"信息路" ,"lat" :"" ,"needgeo" :"0" }},"results" :[{"groupType" :1 ,"resultType" :"url" ,"values" :{"url" :"http://m.elong.com/hotel/0101/nlist/#indate=2018-08-24&outdate=2018-08-25&keywords=%E4%BF%A1%E6%81%AF%E8%B7%AF" }},{"groupType" :1 ,"resultType" :"text" ,"values" :{"text" :"亲,已帮你找到相关酒店信息" }}]}
常见的响应格式
HTTP状态码
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误
HTTP响应码
200: 成功
301/302: 请求重定向到另外一个接口
400: 请求语法错误
403:资源没有访问权限
404:资源不存在(有可能是请求url错误或参数不正确)
405:请求方法不被允许(比如接口只允许Post,使用Get请求接口)
500:服务器内部错误(通常是服务器挂了或接口Bug)
502: 网关失效
504: 网关请求超时
HTTP与HTTPS HTTP协议传输的数据都是未加密的,HTTPS协议是由HTTP+SSL协议构建的可进行加密传输、身份认证的网络协议,要比HTTP协议安全。
HTTPS和HTTP的区别
HTTPS协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
HTTP是超文本传输协议,信息是明文传输,HTTPS则是具有安全性的SSL加密传输协议。
HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
HTTP的连接很简单,是无状态的;HTTPS协议是由HTTP+SSL协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全。
Cookie和Session
*Cookie/Cookies: *是指某些网站为了 辨别用户身份*、 进行session跟踪而 储存在用户本地*终端上的数据(通常经过加密)。
Session: 服务端为客户端访问所建立和维持的会话,通常会生成一个唯一的id,会话有一定的有效期。 由于HTTP是无状态的,即服务器不知道用户上一次做了什么,默认也无法识别用户身份。 比较流行的做法是:
用户访问时服务端建立会话(Session)
将会话id(Session ID)随响应返回,并保存在客户端的Cookies里
后续的访问中,服务器通过辨识,客户端请求时携带的Cookies内容来识别用户
Cookie和Session的区别
cookie是存在客户端(浏览器)的进程内存中和客户端所在的机器硬盘上
cookie只能能够存储少量文本,大概4K大小
cookie是不能在不同浏览器之间共享
Session存在服务器端,存在网站进程的内存中
Session在初次设置session的时候,会在session池中实例化一个session对象,以sessionid 的值作为key,同时会将key以cookie的形式保存到客户端的内存中
Session的作用域只存在当前浏览器的会话中,当浏览器关闭以后就会将sessionid丢失,但是服务器的Session对象要20分钟以后才会回收
授权与加密 常见的接口安全策略:
Session/Cookie机制: 即需要登录,登录后可访问各个接口,最常用的一种策略,适用于内部接口。
固定appid模式: 用户注册时会生成一个唯一的appid,用户调用接口时需要携带appid,适用于公开接口,安全性较差。
动态token模式: token即身份令牌,用户访问接口需要使用个人appid临时申请一个token,token有一定有效期,适用于公开接口,安全性较appid模式好。
开放协议: Basic Auth/ Oauth1.0 / Oauth2.0: 适用于开放接口。
数字签名: 将所有请求参数及参数值进行排列拼接,加上用户私钥,再进行Md5或其他加密生成一个请求的签名(sign),请求是需要携带签名,服务器收到请求后,会对请求重新计算签名并核实与请求所携带签名是否一致。安全性较高,可以有效防止请求被篡改。适用于内部接口及微服务接口。
常见的加密算法 在接口数据传输过程中常对一些敏感数据(如密码)进行Base64编码或MD5加密,以增加安全性。 加密算法分为对称式加密算法和非对称式加密算法,对称式加解密使用同一个秘钥,非对称式使用不同的秘钥。
对称式加密
DES: 数据加密标准,速度较快,适用于加密大量数据的场合
AES: 高级加密标准,速度快,安全级别高
非对称式加密
RSA: 是一个支持变长密钥的公共密钥算法, 分公钥和私钥,SSH协议使用该算法
MD5: 最常用的一种加密方法,是一种摘要算法。
缓存 HTTP 缓存机制作是 web 性能优化的重要手段,当用户第一次请求服务器资源时,服务器将资源缓存到客户端本地,在一定时间内(缓存有效期内)当用户再次向服务器请求同样的资源时,可以直接从缓存中读取,而不用从服务器下载。
接口测试中缓存相关注意点
在更新或调试接口是,注意是否需要清理缓存(或临时禁用缓存)
缓存有一定的有效期
接口性能测试中会关注缓存的命中率
resquests库 requests安装
Windows: 打开cmd命令行,输入pip install requests,等待安装完成即可
Linux: (建议使用Python3),终端中输入pip3 install requests,等待安装完成即可
Mac: (建议使用Python3), sudo python3 -m pip install requests,等待安装完成即可
requests的使用 发送GET请求
组装请求: 请求可能包含url,params(url参数),data(请求数据),headers(请求头),cookies等,最少必须有url
发送请求,获取响应:支持get,post等各种方法发送,返回的是一个响应对象
解析响应: 输出响应文本
1 2 3 4 5 6 7 8 9 import requests url = "http://httpbin.org/get" res = requests.get(url) print (res.text)
带参数的GET请求 1 2 3 4 5 6 7 8 9 10 11 12 13 import requests url = "http://www.tuling123.com/openapi/api?key=ec961279f453459b9248f0aeb6600bbe&info=你好" res = requests.get(url=url) print (res.text) import requests url = "http://www.tuling123.com/openapi/api" params = {"key" :"ec961279f453459b9248f0aeb6600bbe" ,"info" :"你好" } res = requests.get(url=url, params=params) print (res.text)
传统表单类POST请求(x-www-form-urlencoded) 1 2 3 4 5 6 import requests url = "http://httpbin.org/post" data = {"name" : "hanzhichao" , "age" : 18 } res = requests.post(url=url, data=data) print (res.text)
JSON类型的POST请求(application/json) 1 2 3 4 5 6 7 8 9 import requests url = "http://httpbin.org/post" data = '''{ "name": "hanzhichao", "age": 18 }''' res = requests.post(url=url, data=data) print (res.text)
data参数支持字典格式也支持字符串格式,如果是字典格式,requests方法会将其按照默认表单urlencoded格式转换为字符串,如果是字符串则不转化 如果data以字符串格式传输需要遵循以下几点:
必须是严格的JSON格式字符串,里面必须用双引号,k-v之间必须有逗号,布尔值必须是小写的true/false等等
不能有中文,直接传字符串不会自动编码
一般来说,建议将data声明为字典格式(方便数据添加修改),然后再用json.dumps()方法把data转换为合法的JSON字符串格式
1 2 3 4 5 6 7 8 9 10 11 import requests import json url = "http://httpbin.org/post" data = { "name" : "hanzhichao" , "age" : 18 } headers = {"Content-Type" :"application/json" } res = requests.post(url=url, data=json.dumps(data), headers=headers) print (res.text)
或直接将字典格式的data数据赋给post方法的JSON参数(会自动将字典格式转为合法的JSON文本并添加headers)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import requests url = "http://openapi.tuling123.com/openapi/api/v2" data = { "reqType" :0 , "perception" : { "inputText" : { "text" : "附近的酒店" }, "inputImage" : { "url" : "imageUrl" }, "selfInfo" : { "location" : { "city" : "北京" , "province" : "北京" , "street" : "信息路" } } }, "userInfo" : { "apiKey" : "ec961279f453459b9248f0aeb6600bbe" , "userId" : "206379" } } res = requests.post(url=url, json=data) print (res.text)
JSON类型解析 序列化和反序列化 程序中的对象,如Python中的字典、列表、函数、类等,都是存在内存中的,一旦断电就会消失,不方便传递或存储,所以我们需要将内存中的对象转化为文本或者文件格式,来满足传输和持久化(存储)需求
序列化: 内存对象 -> 文本/文件
反序列化: 文本 -> 内存对象
对象在HTTP中的传输过程
HTTP协议是超文本传输协议,是通过文本或二进制进行传输的,所以我们发送的请求要转化成文本进行传输,收到的响应也是文本格式,如果是JSON,一般还需要将文本格式重新转化为对象
JSON对象(Python字典) -> 转为文本请求 -> 发送请求-> 服务器收到文本请求 -> 将文本请求转化为对象,获取其中的参数,处理业务-> 返回文本格式的响应 -> 客户端转为对象格式来从响应中取值
JSON对象与Python字典的区别 JSON对象是javascript object即javascript中的对象,是一种通用的格式,格式严格,不支持备注。
JSON文本和JSON对象的区别:
JSON文本是符合JSON格式的文本,实际上是一个字符串
JSON对象是内存中一个对象,拥有属性和方法,可以通过对象获取其中的参数信息
Python的字典的格式和JSON格式,稍有不同:
字典中的引号支持单引号和双引号,JSON格式只支持双引号
字典中的True/False首字母大写,JSON格式为true/false
字典中的空值为None, JSON格式为null
JSON格式操作方法
序列化(字典 -> 文本/文件句柄): json.dumps()/json.dump()
反序列化(文本/文件句柄 -> 字典) : json.loads()/json.load()
序列化
1 2 3 4 5 import json data = {'name' : '张三' , 'password' : '123456' , "male" : True , "money" : None } str_data = json.dumps(data) print (str_data)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requests import jsonres = requests.post("http://www.tuling123.com/openapi/api?key=ec961279f453459b9248f0aeb6600bbe&info=怎么又是你" ) print (res.text) res_dict = res.json() print (json.dumps(res_dict, indent=2 , sort_keys=True , ensure_ascii=False )) ''' json.dumps() 参数 indent: 缩进空格数,indent=0输出为一行 sork_keys=True: 将json结果的key按ascii码排序 ensure_ascii=Fasle: 不确保ascii码,如果返回格式为utf-8包含中文,不转化为\u... '''
反序列化
1 2 3 4 5 import jsonres_text = '{"name": "\u5f20\u4e09", "password": "123456", "male": true, "money": null}' res_dict = json.loads(res_text) print (res_dict['name' ])
文件的序列化与反序列化
序列化:字典 -> 文件句柄
1 2 3 4 5 import jsonres_dict = {'name' : '张三' , 'password' : '123456' , "male" : True , "money" : None } f = open ("demo1.json" ,"w" ) json.dump(res_dict, f)
序列化: 文件句柄 -> 字典
1 2 3 4 5 6 7 // json 数据 { "name": "张三", "password": "123456", "male": true, "money": null }
1 2 3 4 5 6 import jsonf = open ("demo.JSON" ,"r" , encoding="utf-8" ) f_dict = json.load(f) print (f['name' ]) f.close()
什么时候使用JSON对象(字典)什么时候使用JSON文本? 一般在组装data参数时,建议使用字典格式,发送请求时用json.dumps(data)转化为文本发送,收到请求后使用json.loads(res.text)转化为字典,方便我们获取其中的参数信息
requests库详解 请求方法
equests.get()
requests.post()
requests.put() …
requests.session(): 用于保持会话(session) 除了requests.session()外,其他请求方法的参数都差不多,都包含url,params, data, headers, cookies, files, auth, timeout等等
请求参数
url: 字符串格式,参数也可以直接写到url中
params:url参数,字典格式
data: 请求数据,字典或字符串格式
headers: 请求头,字典格式
cookies: 字典格式,可以通过携带cookies绕过登录
files: 字典格式,用于混合表单(form-data)中上传文件
auth: Basic Auth授权,数组格式 auth=(user,password)
timeout: 超时时间(防止请求一直没有响应,最长等待时间),数字格式,单位为秒
响应解析
res.status_code: 响应的HTTP状态码
res.reason: 响应的状态码含义
req.text:响应的文本格式,按req.encoding解码
req.content: 响应的二进制格式
req.encoding: 解码格式,可以通过修改req.encoding='utf-8'来解决一部分中文乱码问题
req.apparent_encoding:真实编码,由chardet库提供的明显编码
req.json(): (注意,有括号),响应的json对象(字典)格式,慎用!如果响应文本不是合法的json文本,或报错
req.headers: 响应头
req.cookies: 响应的cookieJar对象,可以通过req.cookies.get(key)来获取响应cookies中某个key对应的值
1 2 3 4 5 6 7 8 9 10 11 12 import requests res = requests.get("https://www.baidu.com" ) print (res.status_code, res.reason) print (res.text) print (res.content) print (res.encoding) print (res.apparent_encoding) res.encoding='utf-8' print (res.text) print (res.cookies.items()) print (res.cookies.get("BDORZ" ))
带安全认证的请求 需要登录的请求(Cookie/Session认证)
使用会话保持
1 2 3 4 5 6 import requestss = requests.session() s.post(url="https://demo.fastadmin.net/admin/index/login.html" ,data={"username" :"admin" ,"password" :"123456" }) res = s.get("https://demo.fastadmin.net/admin/dashboard?ref=addtabs" ) print (res.text)
抓取cookies
1 2 3 4 5 6 7 import requestsurl = "https://demo.fastadmin.net/admin/dashboard?ref=addtabs" cookies = {"PHPSESSID" :"9bf6b19ddb09938cf73d55a094b36726" } res = requests.get(url=url, cookies=cookies) print (res.text)
两种方式的对比
使用session方式:每次都要发送两次请求,效率较低
使用携带cookies方式:需要获取cookie,提取组装,cookies中是session有一定有效期,过期之后要重新抓取和更换cookies
如果很多或所有请求都需要登录,可以发一次请求,保持该session为全局变量,其他接口都使用该session发送请求(同样要注意登录过期时间)
appid或token方式
appid: 系统为合法用户赋予的访问id,固定的字符串,一般经过加密以确保HTTP传输中的安全
token: 即令牌,固定或需要动态申请(有一定有效期),一般由用户信息及申请时间计算加密而成,用于验证接口访问的权限
token与session的区别
操作数据库 在功能、接口测试中常常需要通过数据库的操作,来准备数据、检测环境及核对功能、接口的数据库操作是否正确 在自动化测试中,就需要我们用代码连接数据库自动完成数据准备、环境检查及数据库断言的功能。 使用Python操作MySQL数据库这里我们需要用到三方库PyMySQl
安装pymysql
数据库操作
建立数据库连接 conn = pymysql.connect()
从连接建立操作游标 cur = conn.cursor()
使用游标执行sql(读/写) cur.execute(sql)
获取结果(读)/ 提交更改(写) cur.fetchall() / conn.commit()
关闭游标及连接 cur.close();conn.close()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import pymysqlconn = pymysql.connect(host='127.0.0.1' , port=3306 , user='root' , passwd='123456' , db='api_test' , charset='utf8' ) cur = conn.cursor() cur.execute("select * from user where name='张三'" ) result = cur.fetchall() print (result)cur.execute("delete from user where name='李四'" ) conn.commit() cur.close() conn.close()
查询操作 使用cur.execute(), 执行数据库查询后无返回的是影响的行数,而非查询结果。我们要使用cur.fetchone()/cur.fetchmany()/cur.fetchall()来获取查询结果
cur.fetchone(): 获取一条数据(同时获取的数据会从结果集删除),返回元祖('张三','123456')
cur.fetchmany(3): 获取多条数据,返回嵌套元祖(('张三','123456'),('李四','123456'),("王五","123456"))
cur.fetchall(): 获取所有数据,返回嵌套元祖,(('张三','123456'),)(只有一条数据时)
注意: 获取完数据后,数据会从数据集中删除,再次获取获取不到,如:
1 2 3 4 5 6 7 8 9 10 11 cur.execute(select * from user where name='张三' ) print (cur.fetchone()) print (cur.fetchone()) print (cur.fetchall()) cur.execute(select * from user where name='张三' ) result = cur.fetchall() print (result) print (result)
修改操作 执行修改数据库的操作后不立即生效,使用连接conn.commit()提交后才生效,支持事物及回滚
1 2 3 4 5 6 7 try : cur.execute("insert into user (name,password) values ('张三', '123456')" ) cur.execute("insert into user (name, passwd) values ('李四'), '123456'" ) conn.commit() except Exception as e: conn.rollback() print (str (e))
封装数据库操作 由于经常要使用到数据库操作,建议将所有数据库操作封装成公用的数据库模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import pymysqlclass DB : ''' 数据库连接信息建议写到配置文件中,从配置文件中读取 sql语句建议先在手工测试一下没有语法问题再进行封装 通过封装各种sql可以完成各种业务操作 更改数据库有风险,操作需谨慎!!! ''' def __init__ (self ): self.conn = pymysql.connect(host='127.0.0.1' , port=3306 , user='root' , passwd='123456' , db='api_test' ) self.cur = self.conn.cursor() def __del__ (self ): self.cur.close() self.conn.close() def query (self, sql ): self.cur.execute(sql) return self.cur.fetchall() def exec (self, sql ): try : self.cur.execute(sql) self.conn.commit() except Exception as e: self.conn.rollback() print (str (e)) def check_user (self,name ): result = self.query("select * from user where name='{}'" .format (name)) return True if result else False def add_user (name, password ): sql = "insert into user (name, passwd) values ('{}','{}')" .format (name, password) change_db(sql) def del_user (self, name ) self.exec ("delete from user where name='{}'" .format (name))
使用unittest测试框架 用例编写 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import unittest import requestsclass TestUserLogin (unittest.TestCase): url = 'http://115.28.108.130:5000/api/user/login/' def test_user_login_normal (self ): data = {"name" : "张三" , "password" : "123456" } res = requests.post(url=self.url, data=data) self.assertIn('登录成功' , res.text) def test_user_login_password_wrong (self ): data = {"name" : "张三" , "password" : "1234567" } res = requests.post(url=self.url, data=data) self.assertIn('登录失败' , res.text) if __name__ == '__main__' : unittest.main(verbosity=2 )
完整的接口测试用例 一条完整的测试接口用例需要包含:
数据准备:准备测试数据,可手工准备,也可使用代码准备(通常涉及数据库操作)
环境检查:如果手工准备的数据,连接数据库进行环境检查会使用例更健壮
发送请求:发送接口请求
响应断言/数据库断言:响应断言后,还需要进行数据库断言,以确保接口数据库操作的正确性
数据清理:如果接口有更数据库操作,断言结束后需要还原更改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import unittestimport requestsfrom db import * NOT_EXIST_USER = '范冰冰' EXIST_USER = '张三' class TestUserReg (unittest.TestCase): url = 'http://127.0.0.1:5000/api/user/reg/' def test_user_reg_normal (self ): if check_user(NOT_EXIST_USER): del_user(NOT_EXIST_USER) data = {'name' : NOT_EXIST_USER, 'password' : '123456' } res = requests.post(url=self.url, json=data) except_res = { "code" : "100000" , "msg" : "成功" , "data" : { "name" : NOT_EXIST_USER, "password" : "e10adc3949ba59abbe56e057f20f883e" } } self.assertDictEqual(res.json(), except_res) self.assertTrue(check_user(NOT_EXIST_USER)) del_user(NOT_EXIST_USER) def test_user_reg_exist (self ): if not check_user(EXIST_USER): add_user(EXIST_USER) data = {'name' : EXIST_USER, 'password' : '123456' } res = requests.post(url=self.url, json=data) except_res = { "code" : "100001" , "msg" : "失败,用户已存在" , "data" : { "name" : EXIST_USER, "password" : "e10adc3949ba59abbe56e057f20f883e" } } self.assertDictEqual(res.json(), except_res) if __name__ == '__main__' : unittest.main(verbosity=2 )
数据分离 - 从Excel中读取数据 之前的用例中,测试数据直接写在代码文件里,不利于修改和构造数据 这里我们使用Excel保存测试数据,实现代码和数据的分离
excel格式为
Excel读取方法: Python我们使用三方库xlrd来读取Excel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import xlrdwb = xlrd.open_workbook("test_user_data.xlsx" ) sh = wb.sheet_by_name("TestUserLogin" ) print (sh.nrows) print (sh.ncols) print (sh.cell(0 , 0 ).value) print (sh.row_values(0 )) print (dict (zip (sh.row_values(0 ), sh.row_values(1 ))))for i in range (sh.nrows): print (sh.row_values(i)) ''' 3 5 case_name ['case_name', 'url', 'method', 'data', 'expect_res'] {'case_name': 'test_user_login_normal', 'url': 'http://115.28.108.130:5000/api/user/login/', 'method': 'POST', 'data': '{"name": "张三","password":"123456"}', 'expect_res': '<h1>登录成功</h1>'} ['case_name', 'url', 'method', 'data', 'expect_res'] ['test_user_login_normal', 'http://115.28.108.130:5000/api/user/login/', 'POST', '{"name": "张三","password":"123456"}', '<h1>登录成功</h1>'] ['test_user_login_password_wrong', 'http://115.28.108.130:5000/api/user/login/', 'POST', '{"name": "张三","password":"1234567"}', '<h1>失败,用户不存在</h1>'] '''
封装读取excel操作:
我们的目的是获取某条用例的数据,需要3个参数,excel数据文件名(data_file),工作簿名(sheet),用例名(case_name) 如果我们只封装一个函数,每次调用(每条用例)都要打开一次excel并遍历一次,这样效率比较低。 我们可以拆分成两个函数,一个函数excel_to_list(data_file, sheet),一次获取一个工作表的所有数据,另一个函数get_test_data(data_list, case_name)从所有数据中去查找到该条用例的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import xlrddef excel_to_list (data_file, sheet ): data_list = [] wb = xlrd.open_workbook(data_file) sh = wb.sheet_by_name(sheet) header = sh.row_values(0 ) for i in range (1 , sh.nrows): d = dict (zip (header, sh.row_values(i))) data_list.append(d) return data_list def get_test_data (data_list, case_name ): for case_data in data_list: if case_name == case_data['case_name' ]: return case_data if __name__ == '__main__' : data_list = excel_to_list("test_user_data.xlsx" , "TestUserLogin" ) case_data = get_test_data(data_list, 'test_user_login_normal' ) print (case_data)
测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import unittestimport requestsfrom read_excel import * import json class TestUserLogin (unittest.TestCase): @classmethod def setUpClass (cls ): cls.data_list = excel_to_list("test_user_data.xlsx" , "TestUserLogin" ) def test_user_login_normal (self ): case_data = get_test_data(self.data_list, 'test_user_login_normal' ) if not case_data: print ("用例数据不存在" ) url = case_data.get('url' ) data = case_data.get('data' ) expect_res = case_data.get('expect_res' ) res = requests.post(url=url, data=json.loads(data)) self.assertEqual(res.text, expect_res) if __name__ == '__main__' : unittest.main(verbosity=2 )
增加log功能
1 2 3 4 5 6 7 8 9 10 import logginglogging.basicConfig(level=logging.DEBUG, format ='[%(asctime)s] %(levelname)s [%(funcName)s: %(filename)s, %(lineno)d] %(message)s' , datefmt='%Y-%m-%d %H:%M:%S' , filename='log.txt' , filemode='a' ) if __name__ == '__main__' : logging.info("hello" )
Log Level:
CRITICAL: 用于输出严重错误信息
ERROR: 用于输出错误信息
WARNING: 用于输出警示信息
INFO: 用于输出一些提升信息
DEBUG: 用于输出一些调试信息
日志格式:
%(levelno)s: 打印日志级别的数值
%(levelname)s: 打印日志级别名称
%(pathname)s: 打印当前执行程序的路径,其实就是sys.argv[0]
%(filename)s: 打印当前执行程序名
%(funcName)s: 打印日志的当前函数
%(lineno)d: 打印日志的当前行号
%(asctime)s: 打印日志的时间
%(thread)d: 打印线程ID
%(threadName)s: 打印线程名称
%(process)d: 打印进程ID
%(message)s: 打印日志信息
项目使用log 将所有print改为log,如db.py 部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import pymysqlfrom config import *def query_db (sql ): conn = get_db_conn() cur = conn.cursor() logging.debug(sql) cur.execute(sql) conn.commit() result = cur.fetchall() logging.debug(result) cur.close() conn.close() return result def change_db (sql ): conn = get_db_conn() cur = conn.cursor() logging.debug(sql) try : cur.execute(sql) conn.commit() except Exception as e: conn.rollback() logging.error(str (e)) finally : cur.close() conn.close()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import unittestimport requestsfrom read_excel import * import json from config import *class TestUserLogin (unittest.TestCase): @classmethod def setUpClass (cls ): cls.data_list = excel_to_list("test_user_data.xlsx" , "TestUserLogin" ) def test_user_login_normal (self ): case_data = get_test_data(self.data_list, 'test_user_login_normal' ) if not case_data: logging.error("用例数据不存在" ) url = case_data.get('url' ) data = case_data.get('data' ) expect_res = case_data.get('expect_res' ) res = requests.post(url=url, data=json.loads(data)) logging.info("测试用例:{}" .format ('test_user_login_normal' )) logging.info("url:{}" .format (url)) logging.info("请求参数:{}" .format (data)) logging.info("期望结果:{}" .format (expect_res)) logging.info("实际结果:{}" .format (res.text) self.assertEqual(res.text, expect_res) if __name__ == '__main__' : unittest.main(verbosity=2 )
更简单的用例编写 使用用例基类 因为每条用例都需要从excel中读取数据,解析数据,发送请求,断言响应结果,我们可以封装一个BaseCase的用例基础类,对一些方法进行封装,来简化用例编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import unittestimport requestsimport jsonimport syssys.path.append("../.." ) from lib.read_excel import * from lib.case_log import log_case_info class BaseCase (unittest.TestCase): @classmethod def setUpClass (cls ): if cls.__name__ != 'BaseCase' : cls.data_list = excel_to_list(data_file, cls.__name__) def get_case_data (self, case_name ): return get_test_data(self.data_list, case_name) def send_request (self, case_data ): case_name = case_data.get('case_name' ) url = case_data.get('url' ) args = case_data.get('args' ) headers = case_data.get('headers' ) expect_res = case_data.get('expect_res' ) method = case_data.get('method' ) data_type = case_data.get('data_type' ) if method.upper() == 'GET' : res = requests.get(url=url, params=json.loads(args)) elif data_type.upper() == 'FORM' : res = requests.post(url=url, data=json.loads(args), headers=json.loads(headers)) log_case_info(case_name, url, args, expect_res, res.text) self.assertEqual(res.text, expect_res) else : res = requests.post(url=url, json=json.loads(args), headers=json.loads(headers)) log_case_info(case_name, url, args, json.dumps(json.loads(expect_res), sort_keys=True ), json.dumps(res.json(), ensure_ascii=False , sort_keys=True )) self.assertDictEqual(res.json(), json.loads(expect_res))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from test.case .basecase import BaseCaseclass TestUserLogin (BaseCase ): def test_user_login_normal (self ): """level1:正常登录""" case_data = self.get_case_data("test_user_login_normal" ) self.send_request(case_data) def test_user_login_password_wrong (self ): """密码错误登录""" case_data = self.get_case_data("test_user_login_password_wrong" ) self.send_request(case_data)
按用例标签运行 unittest并没有tag相关功能,一种实现方案是添加自定义装饰器
1 2 3 4 5 6 7 8 9 10 11 12 def tag (tag ): if tag==OptionParser.options.tag: return lambda func: func return unittest.skip("跳过不包含该tag的用例" ) ''' 这种方法在最后的报告中会出现很多skipped的用例,可能会干扰到因其他(如环境)原因需要跳过的用例 ''' @tag("level1" ) def test_a (self ): pass
用例标记方法
1 2 3 4 5 6 7 8 9 10 11 12 13 class TestUserLogin (BaseCase ): def test_user_login_normal (self ): """level1:正常登录""" case_data = self.get_case_data("test_user_login_normal" ) self.send_request(case_data) def makesuite_by_tag (tag ): suite = unittest.TestSuite() for case in collect(): if case ._testMethodDoc and tag in case ._testMethodDoc: suite.addTest(case ) return suite
重新运行上次失败用例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import pickleimport sysdef save_failures (result, file ): suite = unittest.TestSuite() for case_result in result.failures: suite.addTest(case_result[0 ]) with open (file, 'wb' ) as f: pickle.dump(suite, f) def rerun_fails (): sys.path.append(test_case_path) with open (last_fails_file, 'rb' ) as f: suite = pickle.load(f) run(suite)
使用命令行参数 为测试执行添加其他自定义命令行参数
1 2 3 4 5 6 7 8 9 10 11 parser = OptionParser() parser.add_option('--collect-only' , action='store_true' , dest='collect_only' , help ='仅列出所有用例' ) parser.add_option('--rerun-fails' , action='store_true' , dest='rerun_fails' , help ='运行上次失败的用例' ) parser.add_option('--testlist' , action='store_true' , dest='testlist' , help ='运行test/testlist.txt列表指定用例' ) parser.add_option('--testsuite' , action='store' , dest='testsuite' , help ='运行指定的TestSuite' ) parser.add_option('--tag' , action='store' , dest='tag' , help ='运行指定tag的用例' ) (options, args) = parser.parse_args()
template & jsonpath Mock Server Mock 即模拟,就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,其最大的优势就是降级前后端耦合度,使前端工程师可以不依赖后端返回数据,先开发前端样式以及逻辑处理
Mock Server 即Mock接口服务器,可以通过配置快速Mock出新的接口
Mock Server的使用范围
前后端分离项目
所测接口依赖第三方系统(还未具备)
所测接口依赖复杂或依赖的接口不稳定,并不作为主要验证对象
Postman还可以基于Collection建立Mock Server,这里不再详述 Python+Flask自己搭建Mock接口 安装依赖
生成mock接口数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from flask import Flask, request, jsonify, abortimport randomapp = Flask(__name__) @app.route("/api/user/reg/" , methods=["POST" ] ) def reg (): if not request.json or not 'name' in request.json or not 'password' in request.json: abort(404 ) res = [ { "code" : "100000" , "msg" : "成功" , "data" : { "name" : "李六" , "password" : "e10adc3949ba59abbe56e057f20f883e" } }, { "code" : "100001" , "msg" : "失败,用户已存在" , "data" : { "name" : "李六" , "password" : "e10adc3949ba59abbe56e057f20f883e" } }, { "code" : "100002" , "msg" : "失败,添加用户失败" , "data" : { "name" : "李六" , "password" : "e10adc3949ba59abbe56e057f20f883e" } } ] return jsonify(random.choice(res)) if __name__ == '__main__' : app.run()