测试中的自动化分为两类:
- 1.ui自动化(web、移动端)
- 2.接口自动化
前面的博客中,我们已经讲解了web端的ui自动化,感兴趣的同学可以去看看:软件测试——自动化测试常见函数_自动化测试代码编写-CSDN博客
今天我们来学习一下接口自动化测试。
想要掌握好接口自动化需要以下技能:
除了Python,其他都会在接下来的章节中讲到
1. 接口测试
我们先了解一下什么是接口测试
1.1 接口的概念
我们可以打开一个网页的开发者工具,然后就可以查看到该网页对应的接口了。
接口一般来说有两种,一种是程序内部的接口,一种是系统对外的接口。
- 程序内部的接口:方法与方法之间,模块与模块之间的交互,程序内部抛出的接口,比如贴吧系统, 有登录模块、发帖模块等等,那你要发帖就必须先登录,要发帖就得登录,那么这两个模块就得有交互,它就会抛出⼀个接口,供内部系统进行调用。
- 系统对外的接口:比如你要从别的网站或服务器上获取资源或信息,别人肯定不会把数据库共享给你,他只能给你提供⼀个他们写好的方法来获取数据,你引用他提供的接口就能使用他写好的方法, 从而达到数据共享的目的,比如说咱们用的app、网址这些它在进行数据处理的时候都是通过接口来进行调用的。
接口类型有很多,如HTTP API接口、RPC等等,接下来我们基于HTTP API接口继续讲解。
1.2 接口测试
1.2.1 概念
接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换,传递和控制管理过程,以及系统间的相互逻辑依赖关系等。 简而言之,所谓接口测试就是通过测试不同情况下的入参与之相应的出参信息来判断接口是否符合或 满足相应的功能性、安全性要求。
其实接口测试很简单,比一般的功能测试还简单,因为功能测试是从页面输入值,然后通过点击按钮或链接等传值给后端,而且功能测试还要测UI、前端交互等功能,但接口测试没有页面,它是通过接口规范文档上的调用地址、请求参数,拼接报文,然后发送请求,检查返回结果,所以它只需测入参和出参就行了,相对来说简单了不少。
1.2.2 接口组成
接口文档应该包含以下内容:
- 接口说明
- 调用url
- 请求方法(get \ post)
- 请求参数、参数类型、请求参数说明
- 返回参数说明
我们可以查看微信官方给出的接口调用凭证文档:接口调用凭证 / 获取稳定版接口调用凭据
由接口文档可知,接口至少应有请求地址、请求方法、请求参数(入参和出参)组成,部分接口有请求头 header
标头(header):是服务器以HTTP协议传HTML资料到浏览器前所送出的字符串,在标头与HTML文件之间尚需空一行分隔,一般存放 cookie 、 token 等信息
header 和入参有什么关系?它们不都是发送到服务器的参数吗?
它们确实都是发送到服务器里的参数,但它们是有区别的,header里存放的参数一般存放的是⼀些校验信息,比如cookie,它是为了校验这个请求是否有权限请求服务器,如果有,它才能请求服务器,然后把请求地址连同入参⼀起发送到服务器,然后服务器会根据地址和⼊参来返回出参。也就是说, 服务器是先接受header信息进行判断该请求是否有权限请求,判断有权限后,才会接受请求地址和入参的。
请求头也是可以从开发者工具中查看到的
1.3 接口测试重要性
接口其实就是前端页面或APP等调用与后端做交互用的,有人会问,功能测试都测好了,为什么还要测接口呢?
先举个栗子:比如测试用户注册功能,规定用户名为6~18个字符,包含字母(区分大小写)、数字、下划线。
首先功能测试时肯定会对用户名规则进行测试时,比如输入20个字符、输入特殊字符等,但这些可能只是在前端做了校验,后端可能没做校验,如果有人通过抓包绕过前端校验直接发送到后端怎么办呢?试想⼀下,如果用户名和密码未在后端做校验,而人又绕过前端校验的话,那用户名和密码不就可以随便输了吗?如果是登录可能会通过SQL注入等手段来随意登录,甚至可以获取管理员权限, 那这样不是很恐怖?
所以,接口测试的必要性就体现出来了:
- 可以发现很多在页面上操作发现不了的bug
- 检查系统的异常处理能力
- 检查系统的安全性、稳定性
- 前端随便变,接口测好了,后端不用变
1.4 如何执行接口测试
在进行接口测试前,还需要了解:
- 1. get和post请求:get和post是常见的请求方法。如果是get请求的话,直接在浏览器里输入就行了,只要在浏览器里面直接能请求到的,都是get请求,如果是post的请求的话,就不行了,就得借助工具来发送。
- 2. http状态码:每发出一个http请求之后,都会有⼀个响应,http本身会有一个状态码,来标示这个请求是否成功,常见的状态码有以下几种:
- 200 2开头的都表示这个请求发送成功,最常见的就是200,就代表这个请求是ok的,服务器也返回了。
- 300 3开头的代表重定向,最常见的是302,把这个请求重定向到别的地方了。
- 400 400代表客户端发送的请求有语法错误,401代表访问的页面没有授权,403表示没有权限访问这个页面,404代表没有这个页面。
- 500 5开头的代表服务器有异常,500代表服务器内部异常,504代表服务器端超时,没返回结果
接口测试分两步走:通过接口设计用例+结合业务逻辑来设计用例
1.4.1 接口用例的编写
1. 通过性验证:首先肯定要保证这个接口功能是好使的,也就是正常的通过性测试,按照接口文档上的参数,正常传入,是否可以返回正确的结果。
2. 参数组合:现在有⼀个操作商品的接口,有个字段type,传1的时候代表修改商品,商品id、商品名称、价格有⼀个是必传的,type传2的时候是删除商品,商品id是必传的,这样的,就要测参数组合了,type传1的时候,只传商品名称能不能修改成功,id、名称、价格都传的时候能不能修改成功。
3. 接口安全:
- 绕过验证,比如说购买了一个商品,它的价格是300元,那我在提交订单时候,我把这个商品的价格改成3元,后端有没有做验证,更狠点,我把钱改成-3,是不是我的余额还要增加?
- 绕过身份授权,比如说修改商品信息接口,那必须得是卖家才能修改,那我传一个普通用户,能不能修改成功,我传一个其他的卖家能不能修改成功
- 参数是否加密,比如说我登陆的接口,用户名和密码是不是加密,如果不加密的话,别人拦截到你的请求,就能获取到你的信息了,加密规则是否容易破解。
- 密码安全规则,密码的复杂程度校验
4. 异常验证:
所谓异常验证,也就是我不按照你接口文档上的要求输入参数,来验证接口对异常情况的校验。比如说必填的参数不填,输入整数类型的,传入字符串类型,长度是10的,传11,总之就是你说怎么来,我就不怎么来,其实也就这三种,必传非必传、参数类型、入参长度。
1.4.2 结合业务逻辑来设计用例
根据业务逻辑来设计的话,就是根据自己系统的业务来设计用例,这个每个公司的业务不⼀样,就得具体的看自己公司的业务了,其实这也和功能测试设计用例是⼀样的。
举个例子,拿贴吧来说,贴吧的需求是这样的:
- 1. 登录失败5次,就需要等待15分钟之后再登录
- 2. 新注册的用户需要过了实习期才能发帖
- 3. 删除帖子扣除积分
- 4. ......
像这样需要把这些测试点列出来,然后再去造数据测试对应的测试点。
2. 接口自动化测试
2.1 概念
接口自动化是通过对接口进行测试和模拟,以确保软件系统内部的各个组件能够正确地相互通信和交换数据。接口自动化测试可以显著提高测试效率和准确性。因为接口测试专注于测试系统内部的逻辑和数据传输,而不是像UI测试那样关注用户的操作和交互。同时,由于接口测试直接针对系统内部的结构和功能,可以更容易地发现和定位问题,减少测试成本和时间。
2.2 接口自动化流程
1. 需求分析
- 分析请求:明确接口的URL、请求方法(如get、post、PUT、DELETE等)、请求头、请求参数和请求体等信息。
- 分析响应:确定接口返回的数据格式、状态码以及可能的错误信息。
2. 挑选自动化接口
- 根据项目的时间、人员安排和接口的复杂度,挑选适合动化测试的接口。
- 优先选择核心业务接口、频繁使用的接口以及容易出错的接口进行自动化测试。
- 功能复杂度:优先选择功能复杂、逻辑分支多的接口进行自动化测试。例如,涉及多种支付方式、多种订单状态转换的订单管理接口,手动测试难以全面覆盖所有场景,自动化测试可以更高效地进行测试.
- 高风险功能:选择对业务影响大、风险高的接口进行自动化测试,确保其稳定性和可靠性。例如,涉及资金操作的支付接口,一旦出现问题可能导致严重的经济损失,因此需要进行充分的自动化测试.
- 重复性高:对于需要频繁执行的测试任务,如回归测试中的接口测试,自动化测试可以避免重复手动测试的繁琐和低效,提高测试效率.
假设我们正在开发⼀个在线教育平台,该平台包含以下接口:
功能复杂度:
- 新增课程接口:涉及多个参数(课程名称、课程描述、课程价格等),需要与其他模块(如课程分类模块)交互。
- 查询课程接口:支持多种查询条件(课程名称、课程类型、课程状态等),逻辑复杂。
- 课程购买接口:涉及支付流程、订单生成等复杂逻辑。
高风险功能:
- 登录接口:用户登录是系统的核心功能,任何问题都会影响用户体验。
- 新增课程接口:课程信息的正确性直接影响平台的运营。
- 用户注册接口:用户注册是系统的基础功能,任何问题都会影响用户获取服务。
重复性高:
- 登录接口:用户每次使用系统都需要登录。
- 查询课程接口:用户频繁查询课程信息。
- 用户信息查询接口:用户经常查看自己的信息。
3. 设计自动化测试用例
- 如果在功能测试阶段已经设计了测试用例,可以直接拿来使用。
- 根据接口需求和功能,设计正向测试用例(正常场景)和反向测试用例(异常场景),包括边界值测试、参数组合测试等。
4. 搭建自动化测试环境
- 选择合适的编程语言(如Python、Java等)和开发环境(如PyCharm、IntelliJ IDEA等)来实现自动化测试。
- 以Python为例,安装必要的依赖库,如requests用于发送HTTP请求,pytest用于测试框架。
5. 设计自动化执行框架
- 设计⼀个框架来执行测试用例,包括报告生成、参数化处理和用例执行逻辑。
6. 编写代码
- 根据设计好的测试用例和框架,编写自动化测试脚本。
7. 执行用例
- 使用测试框架(如unittest、pytest)来执行编写的测试用例。
8. 生成测试报告
- 测试完成后,生成测试报告。可以使用⼯具如HtmlTestRunner或Allure来生成易于阅读的报告。
tips:接口自动化流程是面试考点
2.3 第一个简单的接口自动化
示例:对百度接口发起请求
import requests
r = requests.get("https://www.baidu.com")
print(r)
运行结果:
2.4 requests模块
2.4.1 安装
有些同学可能会遇到如下问题:
这是因为requests包没有下载,我们可以使用命令行通过pip工具进行安装,命令:
pip install requests==2.31.0
我这里使用的是2.32.0的版本,同学可以根据自己的需要来变更版本,安装成功后会出现如下界面
我们还可以检查当前项目下包是否更新:
pip list
2.4.2 介绍
requests 库是一个非常流行的HTTP客户端库,用于发送HTTP请求。
- requests.get 方法用于发送⼀个HTTP get 请求到指定的URL
- requests.get 方法返回⼀个 Response 对象,这个对象包含了服务器返回的所有信息。
如:
Response 对象提供的属性 / 方法介绍:
注意:
- 如果响应格式为JSON格式,则必须以JSON格式打印(不能以text方式打印)
- 如果响应格式为html格式,则必须以text格式打印(不能以JSON方式打印)
演示:
返回的响应为html,所以我们先使用text进行打印
import requests
r = requests.get("https://www.baidu.com")
print(r.status_code) #状态码
print(r.text) #响应体
运行结果没什么问题,现在我们再尝试使用JSON打印一下试试
import requests
r = requests.get("https://www.baidu.com")
print(r.status_code) #状态码
print(r.text) #响应体
print(r.json()) #响应体
运行结果:
原因就是我们使用了错误的打印方式。
查看请求头
我们再来查看一下请求头
import requests
r = requests.get("https://www.baidu.com")
print(r.status_code) #状态码
print(r.text) #响应体
print(r.headers) #响应头
运行结果:
为了方便展示,我将打印的格式修改一下:
我们再来查看从网页的开发者工具中返回的响应头:
可以看到大多数的数据都是一样的,网页查看和代码的方式会有一些差异所以并不是完全一样的。
2.4.3 常见请求方法
requests库中包含了https常用的请求方法:
常用函数:
#发起get请求
def get(url, params=None, **kwargs)
#发起post请求
def post(url, data=None, json=None, **kwargs)
#⽀持不同请求⽅式,method:指定请求⽅法,
#⽀持``get``, ``OPTIONS``, ``HEAD``, ``post``, ``PUT``, ``PATCH``, or ``DELETE``
def request(method, url, **kwargs)
- url:需要请求的资源
- params:一个字典、列表或者字符串,将作为查询字符串附加到URL上(一般是get请求,参数可以拼接在url上)
- kwargs:其他要携带的参数
- data:一个字典、列表或者字符串,包含要发送的请求体数据(post请求,参数是表单格式)
- json:一个字典,将被转换为JSON格式并发送(post请求,参数是JSON格式)
- method:请求方法(想使用什么请求方法,method就填写什么)
使用示例:
import requests
get_r = requests.get('http://www.baidu.com')
post_r = requests.post('http://www.baidu.com')
req1 = requests.request('GET', 'http://www.baidu.com')
req2 = requests.request('POST', 'http://www.baidu.com')
print("get: ", get_r)
print("post: ", post_r)
print("req1: ", req1)
print("req2: ", req2)
结果:
如果我们再使用转包工具来查看响应结果呢?
可以看到使用POST方法得到的响应码是200,和前面使用代码得到的结果不太一样,这个不用太关心,网页和代码方式会存在一些差异。
注意:
requests中的method参数中传大写和小写都是可以的。
2.4.4 添加请求信息
get() 、 post() 底层都是调用 request() 方法,因此这三个方法在发送请求时,传参无太大区别,我们可以查看一下他们的源代码:
可传递的参数展示如下:
演示添加参数场景
例如现在有一个博客系统,在登录成功后会返回一个user_token字段,在往后的请求服务中都需要携带该字段来标明用户身份,如果没有user_token字段则说明该用户是无效身份,不能提供服务,会返回401状态码。
1.登录成功后返回user_token(就是下图中的data)
2.在没有携带user_token字段时请求获取博客列表时,返回401状态码
3.携带了user_token字段时请求获取博客列表时,返回200状态码,成功
总结:上面的场景中必须要携带user_token参数,否则请求失败。
在对这个接口有一定了解后,我们就可以使用代码来编写自动化脚本了。
示例一:博客详情页接口
url = "http://8.137.19.140:9090/blog/getBlogDetail"
# 定义查询参数
params = {
"blogId":9773
}
# 定义请求头信息
header = {
"user_token_header":"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlck5hbWUiOiJ6aGFuZ3Nhb
iIsImV4cCI6MTczNTU1ODk2Mn0.72oh-gQ-5E6_aIcLsjotWL4ZHmgy0jF1794JDE-uUkg",
"x-requested-with":"XMLHttpRequest"
}
r = requests.post(url=url, params=params, headers=header)
print(r.json())
url中没有携带参数,所以我们需要额外定义一个params参数,然后将blogId字段填进去(注意:也可以将参数直接添加到url中,就不需要params参数了)
返回的结果是SUCCESS,也就是正确的,说明user_token字段被成功传递进去了。
示例2:博客登录接口
上面演示了使用user_token的场景,我们再来尝试一下获取user_token,即在登录界面发送请求。
使用抓包工具可以返回正确的data,再来尝试一下代码的方式。
url = "http://8.137.19.140:9090/user/login"
# 定义要发送的数据
data = {
"username":"zhangsan",
"password": "123456"
}
r = requests.post(url=url, data=data)
print(r.json())
运行结果:
可以看到成功返回了user_token字段(data)
示例3:添加 cookie 信息
以博客园接口为例:https://account.cnblogs.com/user/userinfo
未登录状态下的接口返回值:
登录状态下的接口返回值:
两者最主要的区别就是是否携带Cookie信息。
import requests
url = "https://account.cnblogs.com/user/userinfo"
# 定义请求头信息
header = {
"Accept":"application/json, text/javascript, */*; q=0.01"
}
# 定义cookie信息
cookie = {
".Cnblogs.AspNetCore.Cookies":"CfDJ8Ct_7-Gh-gZNte6RB_khjDrB1LrbhEsj64A"
"S3hXoD4Yk6yuxVsSRkWLpG0DxDq89PB2bDWehXw4EnfAQ6fkCdR-zgZ3XAn7ErFBJo9gzyy"
"9PJbhpFo7K5HMO5heP9fq5MZtRIkodlCQ8gDzFsjfUdSaM27-QLTvSA5DUneqqM21WQV0QwWT"
"ZsvP1ZSd2D9m4BwucG4U7lDhCFMqCqDH4LsET_qkqrlMW2Cx4pbtr4VXAmQLmAf0WtJhKRGvGE"
"5vOv71RdeAzEBqso4n0-Cnyv-U7_PEOsUHAGjNagwSxKYcF0Bg3zugkrxrCp0iNffPlTNcGU6e"
"ukI1gz6AF40H8cwNp49vEo7X6QnZscGfPojG4MCApz0MWTFSqHWL0OoqxpbQBBfG2XrsUXltm3D"
"RVM17-suyFTxMr1BkWooKc8JHljXvfofWN6wxlf5p9YRPEfwFa-lVniwlySkvUncrZkjTBcCyw5a"
"Qa0oU_WXhsDKp5BJxk-Efw6SBLtuANJDREqZV7IwcaraeuU9z74C-BVQLSaYDF8JuooUlwhVWPDrJn5bo0YN5mDB32rhYtDGIQUw; "
}
r = requests.post(url=url, headers=header, cookies=cookie)
print(r.json())
运行结果:
成功显示出了登录信息。
问题:上传参数选择 params 、 json 还是 data ?
- params 用于在URL中传递查询参数(Query Parameters),通常用于GET 请求,但也可以用于其他类型的请求。
- json 用于在请求体(Body)中传递 JSON 格式的数据,通常用于POST 或 PUT 请求。
- data 用于在请求体(Body)中传递表单数据,通常用于POST 或 PUT 请求。
注意:若参数上传格式选择为 json 格式, Content-Type 会自动被设置为application/json
我们来看一下源码,只要没有设置data,并且json非空,就将content_type设置为json。
Content-Type既可以在请求头中也可以在响应头中,请求头标识请求参数的格式,响应头表示响应
数据的格式
有了 requests 库,可以实现对接口发起 http 请求,然而自动化测试中,我们需要编写大量的测试用例,这些用例的组织、执行和管理也需要使用其他更强大的框架 ⸺ pytest 框架。
requests 库专注于HTTP请求的发送,而 pytest 框架则提供了测试的组织、执行和管理功能。
2.5 自动化框架pytest
支持Python语言的接口自动化框架有很多,以下是支持Python的接口自动化主流框架对比分析: 主流框架对比表:
2.5.1 pytest介绍
pytest官方文档:Get Started - pytest documentation
pytest 是一个非常流行且高效的Python测试框架,它提供了丰富的功能和灵活的用法,使得编写和运行测试用例变得简单而高效。
为什么选择pytest:
- 简单易用: pytest 的语法简洁清晰,对于编写测试用例非常友好,几乎可以在几分钟内上手。
- 强大的断言库: pytest 内置了丰富的断言库,可以轻松地进行测试结果的判断。
- 支持参数化测试: pytest 支持参数化测试,允许使用不同的参数多次运行同⼀个测试函数,这大大提高了测试效率。
- 丰富的插件⽣态系统: pytest 有着丰富的插件生态系统,可以通过插件扩展各种功能,比如覆盖率测试、测试报告生成(如 pytest-html 插件可以生成完美的HTML测试报告)、失败用例重复执行(如 pytest-rerunfailures 插件)等。此外, pytest 还支持与selenium、requests、appinum等结合,实现Web自动化、接口自动化、App自动化测试。
- 灵活的测试控制: pytest 允许跳过指定用例,或对某些预期失败的case标记成失败,并支持重复执行失败的case。
2.5.2 安装
安装 pytest8.3.2 要求 python 版本在3.8及以上。
pip install pytest==8.3.2
若python版本低于3.8,可参考表格不同的pytest 版本支持的python版本:
安装成功示例:
使用pip list也可以看到pytest被安装成功了。
安装好 pytest 后,确认pycharm中python解释器已经更新,来看⼀下有 pytest 框架和没有pytest 框架编写代码的区别:
未安装:
运行结果:
已安装:
运行结果:
两张对比图可以明显看出来,未安装pytest框架的情况下需要编写 main 函数,在 main 函数中手动调用测试用例test01;安装了 pytest 框架后方法名前有直接运行标志。
然而并不是所有的方法都可以直接运行,需要遵循 pytest 中的用例命名规则。
2.5.3 用例运行规则
- 文件名必须以 test_ 开头或者 _test 结尾
- 测试类必须以 Test 开头,并且不能有 __init__ 方法。
- 测试方法必须以 test 开头
当满足以上要求后,可通过命令行参数 pytest 直接运行符合条件的用例:
我们再创建两个文件,分别写上下面内容:
我们通过命令行输入pytest,就会帮我们运行所有符合命名规则文件中符合规则的函数,所以最终显示运行了7个用例。
注意:Python类中不可以添加init方法
运行结果:
由于 pytest 的测试收集机制,测试类中不可以定义 __init__ 方法。 pytest 采用自动发现机制来收集测试用例。它会自动实例化测试类并调用其所有以 test 结尾的方法作为测试用例。如果测试类中定义了 __init__ 方法,那么当 pytest 实例化该类时, __init__ 方法会被调用,这可能会掩盖测试类的实际测试逻辑,并引入额外的副作用,影响测试结果的准确性。
若测试类中存在初始化操作该采取什么方案?
为了避免使用 __init__ 方法,建议在 pytest 中使用其他替代方案,如使用 setUp() 和tearDown() 方法、使用类属性、使用 fixture 函数(具体使用后续会讲解)
2.5.4 pytest命令参数
pytest 提供了丰富的命令行选项来控制测试的执行。以下是⼀些常用的 pytest 命令行参数及其使用说明。
示例1:运行符合运行规则的用例
pytest
注意,这里不会输出测试用例中printf内容
示例2:详细打印,并输入print内容
pytest -s -v 或者 pytest -sv (可以连写)
示例3:指定文件 / 测试用例
#指定⽂件:pytest 包名/⽂件名
pytest cases/test_01.py
#指定测试⽤例: pytest 包名/⽂件名::类名::⽅法名
pytest cases/test_01.py::Test::test_a
问题:当我们既要详细输出,又要指定文件时,命令会又臭又长,而且每次运行都需要手动输入命令,如何解决?
将需要的相关配置参数统一放到 pytest 配置文件中
2.5.5 pytest配置文件
在当前项目下创建 pytest.ini 文件,该文件为 pytest 的配置文件
以下为常见的配置选项:
示例:详细输出 cases 包下文件名以 test_ 开头且类名以 A 开头的所有用例
[pytest]
addopts = -vs
testpaths = ./cases
python_files = test_*.py
python_classes = A*
目录结构如下:
若cases目录下3个test文件内容如下:
预期结果:
- 因为test02文件不符合 python_files 的命名规则,所以该文件下的用例不会被执行
- test_03文件下的Test3不符合 python_classes 的命名规则,所以也不会被执行
- 最终执行的是test_01文件下的 test_requests 和 test_03 文件下A3目录的test03_01函数
配置好 pytest.ini 文件后,命令行执行 pytest 命令即可,无需再额外指定其他参数:
符合我们的预期。
pytest.ini 文件通常位于项目的根目录下。通过在 pytest.ini 中定义配置项,可以覆盖pytest 的默认行为,以满足项目的需求。
2.5.6 前后置
遗留问题:使用 pytest 框架,测试类中不可以添加init()方法,如何进行数据的初始化?
在测试框架中,前后置是指在执行测试用例前和测试用例后执行一些额外的操作,这些操作可以用于设置测试环境、准备测试数据等,以确保测试的可靠性
pytest 框架提供三种方法做前后置的操作:
- setup_method 和 teardown_method :这两个方法用于类中的每个测试方法的前置和后置操作。
- setup_class 和 teardown_class :这两个方法用于整个测试类的前置和后置操作。
- fixture :这是 pytest 推荐的方式来实现测试用例的前置和后置操作。 fixture 提供了更灵活的控制和更强大的功能。(该内容后续在 fixture 章节中详细讲解)
示例1: setup_method 和 teardown_method
class Test4:
def setup_method(self):
print("setup_method()")
def teardown_method(self):
print("teardown_method()")
def test_04_01(self):
print("test_04_01():")
def test_04_02(self):
print("test_04_02():")
运行结果:
每个用例执行之前都会调用setup_method(),每个用例执行之后都会调用teardown_method()。
示例2: setup_class 和 teardown_class
class Test04:
def setup_class(self):
print("setup_class():")
def teardown_class(self):
print("teardown_class():")
def test_04_01(self):
print("test_04_01():")
def test_04_02(self):
print("test_04_02():")
运行结果:
所有用例执行之前会调用setup_class(),所有用例执行之后会调用teardown_class()。
2.5.7 断言
断言( assert )是⼀种调试辅助工具,用于检查程序的状态是否符合预期。如果断言失败(即条件为假),Python解释器将抛出一个 AssertionError 异常。断言通常用于检测程序中的逻辑错误。
pytest 允许你在 Python 测试中使用标准的 Python assert 语句来验证预期和值。
基本语法:
assert 条件, 错误信息
- 条件 :必须是⼀个布尔表达式。
- 错误信息 :当条件为假时显⽰的错误信息,可选。
免费学习API资源:JSONPlaceholder - Free Fake REST API
示例1:基本数据类型的断言
def test_05_01():
a = 1
b = 1
assert a == b
str1 = "hello"
str2 = "world"
assert str1 == str2
断言失败后会给我们提示信息告诉我们哪里出错了
示例2:数据结构断言
def test_05_02():
#断言列表
expect_list1 = [1, "hello", 3.14]
expect_list2 = [1, "hello", 3.14]
assert expect_list1 == expect_list2
#断言元组
expect_tuple1 = (1, "apple", 3.14)
expect_tuple2 = (1, "apple", 3.14)
assert expect_tuple1 == expect_tuple2
#断言字典
expect_dict1 = {"apple": 3.14}
expect_dict2 = {"applef": 3.14}
assert expect_dict1 == expect_dict2
#断言集合
expect_set1 = {1, 2, 3, "apple"}
expect_set2 = {1, 2, 3, "apple"}
assert expect_set1 == expect_set2
运行结果:
提示断言失败的信息还是比较直观明显的
示例3:函数断言
def divide(a, b):
assert b != 0, "除数不能为0"
return a / b
def test_05_03():
# 正常情况
print(divide(10, 2)) # 输出 5.0
# 触发断⾔
print(divide(10, 0)) # 抛出 AssertionError: 除数不能为0
运行结果:
示例4:接口返回值断言
我们使用前面给的学习API的网站JSONPlaceholder - Free Fake REST API 往下翻可以看到给了很多的请求方法
我们选择/posts
可以看到返回了很多数据
我们选择posts/1即可
接下来我们可以尝试访问这个接口,得到的返回值是不是上面给出的响应数据。
def test_05_04():
url = "http://jsonplaceholder.typicode.com/posts/1"
r = requests.get(url=url)
expect_data = {
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
actual_data = r.json()
assert actual_data == expect_data
运行结果:
我们再修改一个数据,让实际结果和预期结果不一样再来观察一下,这里将id由原来的1改成2
可以看到立马就指出了不一样的地方。
如果响应结果比较复杂,如何进行断言?
例如下图这种场景,json里面还会嵌套json,并且响应数据非常多,我们不可能把所有的都拷贝过去进行比较。
这种情况下我们可以对关键字段进行校验,例如上面的每组的name,email,body可能一样,但是id一定不一样,所以我们就对id进行校验
def test_05_05():
url = "http://jsonplaceholder.typicode.com/comments?posts/1"
r = requests.get(url=url)
assert r.json()[0]["id"] == 1
assert r.json()[1]["id"] == 2
assert r.json()[2]["id"] == 4
运行结果:
响应结果是text格式,可以打印吗?
我们查看到改网站的主页面的响应结果是text,我们接下来就查看该网站能否找到Use your won data这个文本
编写代码:
def test_05_06():
url = "http://jsonplaceholder.typicode.com/"
r = requests.get(url=url)
text = "Use your own data"
assert text in r.text
再来看一下断言失败的结果:
最终也是成功表示该字符串没有找到。
2.5.8 参数化
参数化设计是自动化设计中的一个重要组成部分,它通过定义设计参数和规则,使得设计过程更加灵活和可控。
假如现在有一个邮箱登录页面,内部由两个文本输入框,如果针对这两个输入框来设计测试用例其实是可以设计很多的,比如可以设计正常登录情况,异常登录情况等。
我们针对每一个测试用例都需要写一个test函数进行测试,但其实每个test函数内部逻辑都是一样的,只是email和password两个参数不同。
针对上面这个问题,我们可以使用参数化的方式来解决,即将email和password作为函数参数。
pytest中内置的 pytest.mark.parametrize 装饰器允许对测试函数的参数进行参数化。
示例1:单个参数使用参数化
@pytest.mark.parametrize("data", (1, 2, 3, 4))
def test_06_02(data) :
print("test_06_02() -> ", data)
这里, @parametrize 装饰器的第一个参数表示要参数化的变量名,第二个元组表示该变量的所有取值。
运行结果:
可以看到参数化将parametrize中元组参数中的所有取值用例都执行了一遍。
注意,python中的元组可以是不同的数据类型。
示例2:多个参数使用参数化
import pytest
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6),("6*9", 42)])
def test_eval(test_input, expected):
assert eval(test_input) == expected
这里, @parametrize 装饰器定义了三个不同的 (test_input,expected) 元组,以便test_eval 函数将依次使用它们运行三次。
也可以在类或模块上使用 parametrize 标记,这将使用参数集调用多个函数
示例3:在类上使用参数化
import pytest
@pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
def test_simple_case(self, n, expected):
assert n + 1 == expected
def test_weird_simple_case(self, n, expected):
assert (n * 1) + 1 == expected
运行结果:
类中每个符合规则的方法都会执行列表参数,所以一共执行了4次。
要对模块中的所有测试进行参数化,你可以将 pytestmark 全局变量赋值:
@pytest.mark.parametrize("data", (1, 2))
class TestA:
def testA_01(self, data) :
print("testA_01(): ", data)
def testA_02(self, data) :
print("testA_02(): ", data)
class TestB:
def testB_01(self, data):
print("testB_01(): ", data)
def testB_02(self, data):
print("testB_02(): ", data)
针对上面这个代码,其实会运行错误,因为TestB中的data没有参数。
解决方法也很简单,直接将TestA上的参数化拿过来即可,然后这又会涉及到重复的问题,为了解决这个问题,我们将 pytestmark 全局变量赋值。
pytestmark = pytest.mark.parametrize("data", (1, 2))
class TestA:
def testA_01(self, data) :
print("testA_01(): ", data)
def testA_02(self, data) :
print("testA_02(): ", data)
class TestB:
def testB_01(self, data):
print("testB_01(): ", data)
def testB_02(self, data):
print("testB_02(): ", data)
运行结果:
除了使用 @parametrize 添加参数化外, pytest.fixture() 允许对 fixture 函数进行参数化
示例4:自定义参数化数据源
大多数场景中我们的参数并不是写死的,而是其他函数的返回值。
def data_provider():
return ["a", "b"]
# 定义⼀个测试函数,它依赖于上⾯函数的返回值
@pytest.mark.parametrize("data", data_provider())
def test_data(data):
assert data != None
print(f"Testing with data provider: {data}")
运行结果:
2.5.9 fixture
pytest 中的 fixture 是一种强大的机制,用于提供测试函数所需的资源或上下文。它可以用于设置测试环境、准备数据等。以下是 fixture 的一些核心概念和使用场景。
2.5.9.1 基本使用
示例1:使用与不使用fixture标
未使用fixture:
#未标记fixture方法的调用——函数名来调用
def fixture_01():
print("第一个fixture方法")
def test_01():
fixture_01()
print("第一个测试用例")
使用fixture:
@pytest.fixture
def fixture_01():
print("第一个fixture方法")
def test_02(fixture_01):
print("第二个测试用例")
未标记 fixture 方法的调用与 fixture 标记的方法调用完全不⼀样,前者需要在方法体中调用,而后者可以将函数名作为参数进行调用。
测试脚本中存在的很多重复的代码、公共的数据对象时,使用 fixture 最为合适
示例2:访问列表页和详情页之前都需要执行登录操作
import pytest
@pytest.fixture
def login():
print("---执⾏登陆操作-----")
def test_list(login):
print("---访问列表⻚")
def test_detail(login):
print("---访问详情⻚")
运行结果:
通过使用 @pytest.fixture 装饰器来告诉 pytest ⼀个特定函数是⼀个 fixture,通过运行结果可见,在执行列表页和详情页之前都会先执行 login 方法。
2.5.9.2 fixture嵌套
@pytest.fixture
def first():
print("First")
@pytest.fixture
def second(first):
print("Second")
def test(second):
print("Test")
运行结果:
比较两个列表是否相等
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
order.append("b")
assert order == ["a", "b"]
运行结果:
测试不必局限于单个 fixture ,它们可以依赖于您想要的任意数量的 fixture ,并且 fixture 也可以使用其他 fixture 。 pytest 最伟大的优势之一是其极其灵活的 fixture 系统,它允许我们将测试的复杂需求简化为更简单和有组织的函数,我们只需要每个函数描述它们所依赖的事物。
2.5.9.3 请求多个fixture
class Fruit:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
@pytest.fixture
def my_fruit():
return Fruit("Apple")
@pytest.fixture
def your_fruit(my_fruit):
return [my_fruit, Fruit("Banana")]
def test_fruit(my_fruit, your_fruit):
assert my_fruit in your_fruit
注意:pytest的需要测试的类(以Test开头)不可以设置__init__函数,其他不需要收集的类是可以设置__init__函数的。
上面代码的意思是Fruit有一个水果类,my_fruit函数表示我有一个Apple,your_fruit函数会调用my_firit,并且自己有一个Banana,所以your_fruit中有Apple和Banana。最后再调用test_fruit来测试my_fruit是否属于your_fruit。
测试和 fixture 不仅限于⼀次请求单个 fixture ,它们可以请求任意多个。
2.5.9.4 yield fixture
当我们运行测试时,我们希望确保它们能够自我清理,以便它们不会干扰其他测试(同时也避免留下大量测试数据来膨胀系统)。pytest中的 fixture 提供了⼀个非常有用拆卸系统,它允许我们为每个 fixture 定义具体的清理步骤。
“Yield” fixture 使用 yield 而不是 return 。有了这些 fixture ,我们可以运行一些代码,并将对象返回给请求的 fixture/test ,就像其他 fixture ⼀样。唯⼀的不同是:
- return 被替换为 yield 。
- 该 fixture 的任何拆卸代码放置在 yield 之后。
一旦 pytest 确定了 fixture 的线性顺序,它将运行每个 fixture 直到它返回或 yield ,然后继续执行列表中的下⼀个 fixture 做同样的事情。
测试完成后, pytest 将逆向遍历 fixture 列表,对于每个 yield 的 fixture ,运行 yield语句之后的代码。
示例1
@pytest.fixture
def operator():
print("前置操作:数据初始化")
yield
print("后置操作:数据清洗")
def test_operator(operator):
print("测试用例")
运行结果:
执行顺序:
- 先执行operator函数,打印了第一句话。
- 然后遇到yield直接返回,到第二个函数test_operator,打印第二句话。
- 所有用例都执行完之后,开始遍历fixture列表,执行yield语句之后的代码。
示例2:yiele返回值
@pytest.fixture
def operator():
print("前置操作:数据初始化")
yield 100
print("后置操作:数据清洗")
def test_operator(operator):
print("测试用例, operator():", operator + 100)
运行结果:
示例2:创建文件句柄与关闭文件
@pytest.fixture
def file_read():
print("打开文件句柄")
file = open("file.txt", "r", encoding="utf-8")
yield file
print("关闭文件句柄")
file.close()
def test_file(file_read):
r = file_read
str = r.read()
print(str)
file.txt文件内容如下:
运行结果:
没有什么问题,接下来我们再加入写文件
@pytest.fixture
def file_read():
print("打开文件句柄")
file = open("file.txt", "r", encoding="utf-8")
yield file
print("关闭文件句柄")
file.close()
@pytest.fixture
def file_write():
print("打开文件句柄")
file = open("file.txt", "w", encoding="utf-8")
yield file
print("关闭文件句柄")
file.close()
def test_file(file_read, file_write):
#向文件中写入数据
w = file_write
w.write("Hello CSDN")
#读取到刚刚写入的数据
r = file_read
str = r.read()
print(str)
运行结果:
结果看上去没什么问题,但是这段代码存在一个小小的bug,首先我们先打开文件句柄,并向文件中写入数据,写完后该文件句柄仍然是打开状态,并没有被关闭。(虽然我们的file_write中在yield后添加了关闭文件句柄的代码,但是yield后面的代码是在整个用例执行之后才会被调用)
我们向在写完数据之后将文件句柄关闭,下次使用时再重新打开,如果直接在读取完数据之后添加一个close代码,这种方式也不可行,因为在用例结束后又会重新调用一次file_write中的close导致重复关闭问题。
为了解决这个问题,我们只需要将file_weite中的关闭文件句柄的代码删去,后续使用时手动调用close。
2.5.9.5 带参数的fixture
pytest.fixture(scope='', params='', autouse='', ids='', name='')
参数详解:
- scope 参数用于控制fixture的作用范围,决定了fixture的生命周期。可选值有:
- function (默认):每个测试函数都会调用一次fixture。
- class :在同一个测试类中共享这个fixture。
- module :在同一个测试模块中共享这个fixture。(一个文件里)
- session :整个测试会话中共享这个fixture。
- autouse 参数默认为 False 。如果设置为 True ,则每个测试函数都会自动调用该fixture,无需显式传入
- params 参数用于参数化fixture,支持列表传入。每个参数值都会使fixture执行⼀次,类似于for循环
- ids 参数与 params 配合使用,为每个参数化实例指定可读的标识符(给参数取名字)
- name 参数用于为fixture显式设置一个名称。如果使用了 name ,则在测试函数中需要使用这个名称来引用 fixture (给fixture取名字)
2.5.9.5.1 scope
示例1: scope 的使用
scope="function":
@pytest.fixture(scope="function")
def test_fixture01():
print("test_fixture01(): 初始化")
yield
print("test_fixture01(): 数据清洗")
class Test1:
def test_01(self, test_fixture01):
print("test_01(): ")
def test_02(self, test_fixture01):
print("test_02(): ")
function针对的是函数,每个函数调用之前都会去调用test_fixture01()函数yield之前的语句,每个函数调用完之后都会去调用test_fixture01()函数yield之后的语句
scope="class":
@pytest.fixture(scope="class")
def test_fixture01():
print("test_fixture01(): 初始化")
yield
print("test_fixture01(): 数据清洗")
class Test1:
def test_01(self, test_fixture01):
print("test_01(): ")
def test_02(self, test_fixture01):
print("test_02(): ")
class Test2:
def test_03(self, test_fixture01):
print("test_03(): ")
def test_04(self, test_fixture01):
print("test_04(): ")
function针对的是类,每个类调用之前都会去调用test_fixture01()函数yield之前的语句,每个类调用完之后都会去调用test_fixture01()函数yield之后的语句
结论:
- scope 默认为 function ,这里的 function 可以省略不写,当 scope="function" 时,每个测试函数都会调用一次 fixture 。 scope="class" 时,在同⼀个测试类中, fixture只会在类中的第⼀个测试函数开始前执行一次,并在类中的最后一个测试函数结束后执行清理。
- 当 scope="moudle" 、 scope="session" 时可用于实现全局的前后置应用,这里需要多个文件的配合。
示例2: scope="moudle" 、 scope="session" 实现全局的前后置应用
scope="module":
import pytest
@pytest.fixture(scope="module")
def test_fixture01():
print("test_fixture01(): 初始化")
yield
print("test_fixture01(): 数据清洗")
class Test1:
def test_01(self, test_fixture01):
print("test_01(): ")
def test_02(self, test_fixture01):
print("test_02(): ")
class Test2:
def test_03(self, test_fixture01):
print("test_03(): ")
def test_04(self, test_fixture01):
print("test_04(): ")
运行结果:
function针对的是文件,每个文件开始执行第一个用例之前都会去调用test_fixture01()函数yield之前的语句,每个类执行完所有用例之后之后都会去调用test_fixture01()函数yield之后的语句
上面是只有一个文件的场景,那如果是多个文件呢?
conftest.py 和 @pytest.fixture 结合使用实现全局的前后置应用
@pytest.fixture 与 conftest.py 文件结合使用,可以实现在多个测试模块( .py )文件中共享前后置操作,这种结合的方式使得可以在整个测试项目中定义和维护通用的前后置逻辑,使测试代码更加模块化和可维护。
规则:
- conftest.py 是⼀个单独存放的夹具配置文件,名称是固定的不能修改
- 你可以在项目中的不同目录下创建多个 conftest.py 文件,每个 conftest.py 文件都会对其所在目录及其子目录下的测试模块生效。
- 在不同模块的测试中需要用到 conftest.py 的前后置功能时,不需要做任何的import导入操作
- 作用:可以在不同的 .py 文件中使用同一个 fixture 函数
此时我们不能只修改test_cases_02.py文件当中的scope为module,当测试用例存在不同的文件时,需要将fixture放到配置conftest.py文件中。
运行结果:
scope="session":
只要修改conftest.py文件中的scope属性为session即可。
运行结果:
2.5.9.5.2 autouse
autouse 参数默认为 False 。如果设置为 True ,则每个测试函数都会自动调用该fixture,无需显式传入
import pytest
@pytest.fixture(autouse=True)
def test_fixture01():
print("test_fixture01(): 初始化")
yield
print("test_fixture01(): 数据清洗")
class Test5:
def test_09(self):
print("test_09(): ")
def test_10(self):
print("test_010(): ")
运行结果:
- autouse 默认为 False ,即当前的 fixture 需要手动显示调用,在该案例之前我们默认使用的都是 autouse=False
- 当 autouse=True 时, fixture 会在所有测试函数执行之前自动调用,无论这些测试函数是否显式地引用了该 fixture
2.5.9.5.3 params
params 参数用于参数化fixture,支持列表传入。每个参数值都会使fixture执行一次,类似于for循环。
@pytest.fixture(params=[1, 2, 3])
def fixture(request):
return request.param
def test_fixture(fixture):
print(fixture)
运行结果:
前面我们已经学过pytest中通过 @pytest.mark.parametrize 实现参数化,通过 fixture 也可以实现参数化,那么到底哪⼀种更好呢?
如果测试场景主要涉及简单的参数传递,且不需要复杂的资源管理,建议使用 parametrize,因为它更简单直接;如果测试需要动态加载外部数据,或者需要管理复杂的测试资源(如数据库连接、文件操作等),建议使用 fixture,在某些情况下,也可以结合使用 parametrize 和 fixture,以充分利用两者的优点。总结来说,parametrize 更适合简单场景,而 fixture 更适合需要动态数据和资源管理的复杂场景。
2.6 YAML
官方文档:pyyaml.org/wiki/PyYAMLDocumentation
YAML是⼀种数据序列化语言,用于以人类可读的形式存储信息。它最初代表“Yet Another Markup Language”,但后来更改为“ YAML Ain’t Markup Language”(YAML不是一种标记语言),以区别于真正的标记语言。
它类似于XML和JSON文件,但使用更简洁的语法。
特点:
- YAML 是一种非常简单的基于文本的人类可读的语言,用于在人和计算机之间交换数据。
- YAML 不是一种编程语言。它主要用于存储配置信息。
- YAML 的缩进就像 Python 的缩进一样优雅。
- YAML 还减少了 JSON 和 XML 文件中的大部分“噪音”格式,例如引号、方括号和大括号。
注意:
- YAML 是区分大小写。
- YAML 不允许使用制表符 Tab 键,(你之所按下 Tab YAML 仍能使用,是因为编辑器被配置为按下Tab 键会导致插入适当数量的空格)。
- YAML 是遵循严格缩进的。
2.6.1 YAML介绍
YAML 文件的后缀名是 .yaml 或 .yml ,本着能少写不多写的原则,我们常用的是 .yml 。yaml 中支持不同数据类型,但在写法上稍有区别,详见下表:
以上语法若短时间内无法掌握,我们也有很多工具可供使用,如json转yaml:JSON 转 YAML 工具 | 简化数据格式转换 - 嘉澍工具
2.6.2 使用
yaml 文件通常作为配置文件来使用,可以使用 yaml 库来读取和写入 YAML 文件
1.安装yaml库:
pip install PyYAML==6.0.1
2.创建yaml文件:
3.读取和写入yaml文件:
写入yaml文件所使用的函数:
def safe_dump(data, stream=None, **kwds):
return dump_all([data], stream, Dumper=SafeDumper, **kwds)
- data表示要写入的数据
- stream是文件流(从哪个文件,以什么方式写入)
import yaml
def write_yaml(data):
with open("../test.yaml", encoding="utf-8", mode='a+') as f:
yaml.safe_dump(data, f)
def test_write_yaml():
data = {
"name":"zhangsan",
"age":28
}
write_yaml(data)
运行结果:
可以看到yaml与json相比,去除掉了多余的括号和引号
读取yaml文件所使用的函数:
def safe_load(stream):
return load(stream, SafeLoader)
- stream是文件流(从哪个文件,以什么方式读取)
def read_yaml():
with open("../test.yaml", encoding="utf-8", mode='r') as f:
return yaml.safe_load(f)
def test_read_yaml():
data = read_yaml()
print(data)
运行结果:
清空yaml文件:
注意该方法并非yaml文件提供的,因为yaml文件也是一个文件,所以我们使用python自带的清空函数truncate
def clear_yaml():
with open("../test.yaml", encoding="utf-8", mode='w') as f:
f.truncate()
def test_clear_yaml():
clear_yaml()
运行结果:
前面的json格式较为简单,我们再来看一个复杂的json转成yaml是什么样子的。
data = {
"app": "WeatherTracker",
"version": "2.1.4",
"active": True,
"features": {
"current": ["temperature", "humidity", "wind"],
"forecast": ["daily", "hourly", "alerts"]
},
"locations": {
"city": "Tokyo",
"coords": [35.6762, 139.6503],
"active": True
},
"lastUpdated": "2023-08-15"
}
结果为:
我们可以和JSON来对比一下
可以明显观察到大部分多余的引号,方括号,大括号被去掉了,更加的简洁
2.7 JSON Schema
JSON Schema一个用来定义和校验JSON的web规范,简而言之,JSON Schema是用来校验json是否符合预期。
根据 json 创建 JSON Schema 后,你可以使用你选择的语言中的验证器将示例数据与你的模式进行验证。
2.7.1 安装
pip install jsonschema==4.23.0
2.7.2 介绍
通过上面的对比可见, JSON Schema 从多个方面对 JSON 数据进行校验。
如“ type ”、“ required ”、“ properties ”等以确保其正确性和一致性。接下来我们来了解 JSON Schema 中的关键词以及作用。
json转JSON Schema太麻烦?使用现有工具自动转换:在线JSON转Schema工具 - ToolTT在线工具箱
注意:工具不是万能的,结果可能存在错误,要对自动生成的结果进行二次检查
上面转换的JSON Schema是正确的吗?我们可以来验证一下。jsonschema库中有一个validate包,可以用于验证JSON转换的JSON Schema是否正确。
def validate(instance, schema, cls=None, *args, **kwargs):
- instance:要验证的原生数据
- schema:instance对应的JSON Schema
validate主要验证的是:
- 返回的字段是否都存在
- 返回的字段名都是一致的
- 保证数据的类型是一致的
代码:
from jsonschema import validate
def test_jsonSchema():
data = {
"code": "SUCCESS",
"errMsg": "",
"data": False
}
json_schema = {
"type": "object",
"required": [],
"properties": {
"code": {
"type": "string"
},
"errMsg": {
"type": "string"
},
"data": {
"type": "string"
}
}
}
validate(data, json_schema)
运行结果:
可以看到有不一样的地方可以成功校验出来。
示例:校验百度搜索“测试”返回的json数据
先通过代码获取返回的JSON:
def test_jsonSchema2():
url = "https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&wd=测试"
response = requests.get(url)
print(response.json())
运行结果:
{
"q": "测试",
"p": false,
"g": [
{
"type": "sug",
"sa": "s_1",
"q": "测试抑郁程度的问卷"
},
{
"type": "sug",
"sa": "s_2",
"q": "测试infp人格"
},
{
"type": "sug",
"sa": "s_3",
"q": "测试网速"
},
{
"type": "sug",
"sa": "s_4",
"q": "测试抑郁症心理测试题免费"
},
{
"type": "sug",
"sa": "s_5",
"q": "测试MBTI人格免费"
},
{
"type": "sug",
"sa": "s_6",
"q": "测试培训"
},
{
"type": "sug",
"sa": "s_7",
"q": "测试智商的测试题免费"
},
{
"type": "sug",
"sa": "s_8",
"q": "测试的英文"
},
{
"type": "sug",
"sa": "s_9",
"q": "测试工程师"
},
{
"type": "sug",
"sa": "s_10",
"q": "测试反应速度"
}
],
"slid": "50153000091310",
"queryid": "0x2252d9d27c13eae"
}
然后我们再将上面得到的JSON转换成Schema。
{
"type": "object",
"required": [],
"properties": {
"q": {
"type": "string"
},
"p": {
"type": "string"
},
"g": {
"type": "array",
"items": {
"type": "object",
"required": [],
"properties": {
"type": {
"type": "string"
},
"sa": {
"type": "string"
},
"q": {
"type": "string"
}
}
}
},
"slid": {
"type": "string"
},
"queryid": {
"type": "string"
}
}
}
接下来我们就可以测试了,通过validate校验。
def test_jsonSchema2():
url = "https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&wd=测试"
response = requests.get(url)
schema_data = {
"type": "object",
"required": [],
"properties": {
"q": {
"type": "string"
},
"p": {
"type": "string"
},
"g": {
"type": "array",
"items": {
"type": "object",
"required": [],
"properties": {
"type": {
"type": "string"
},
"sa": {
"type": "string"
},
"q": {
"type": "string"
}
}
}
},
"slid": {
"type": "string"
},
"queryid": {
"type": "string"
}
}
}
validate(response.json(), schema_data)
运行结果:
原因和前面的例子一样,type是bool类型,结果被转换成了字符串。
所以说我们不能过度依赖工具,工具生成完之后需要手工检查一遍。
"p": {
"type": "boolean"
},
将p的类型转化成boolean之后就可以成功运行了。
2.7.2.1 数据类型
type 关键字指定了数据类型。
可以验证 JSON 数据中每个属性的数据类型是否符合预期。常用的数据类型包括:
示例:
- type表示当前的JSON对象
- properties 是一个验证关键字。当你定义 properties 时,你创建了一个对象,其中每个属性代表正在验证的 JSON 数据中的一个键。
2.7.2.2 最大最小值
- minimum 和 maximum :指定数值的最小值和最大值。
- exclusiveMinimum 和 exclusiveMaximum :指定数值必须严格大于或小于某个值(不包含等于)。
我们手动为age字段设置minimum和maximum。
现在的age是18,符合区间范围(0~100),所以调用validate函数应该是可以通过的。
如果将age修改成130呢?
可以看到成功提示:130超过了最大值。
minimum 和 maximum表示的是大于等于(>=)和小于等于(<=)。
如果不想要等于的话就需要使用到exclusiveMinimum 和 exclusiveMaximum。
运行结果:
2.7.2.3 字符串特殊校验
• pattern :使用正则表达式来验证字符串是否符合特定的模式。
json_schema = {
"type": "object",
"required": [],
"properties": {
"name": {
"type": "string",
"pattern": "\S{2,20}"
},
"age": {
"type": "number",
}
}
}
如上面的name的pattern我们设置为正则表达式。\S表示匹配一个非空字符,{2,20}表示长度要在2到20之间。所以这个正则表达式匹配的就是长度在2~20之间的字符串。(注意:这里并不是说超过20的字符串就匹配不到,20表示的是最多匹配,即匹配前20个字符也算成功匹配)
zhangsan这个字符串长度在2~20之间,所以可以通过测试用例
我们再把匹配规则的长度调整到{10,20}
2.7.2.4 数组约束
- minItems 和 maxItems :指定数组的最小和最大长度。
- uniqueItems :确保数组中的元素是唯⼀的。
- items :定义数组中每个元素的类型和约束。
JSON和Schema如下:
minItems 和 maxItems :指定数组的最小和最大长度。
设置最小1个,最大10个。
运行成功:
再把最大调整为5,观察一下运行失败的情况:
uniqueItems :确保数组中的元素是唯⼀的。
运行结果:
items :定义数组中每个元素的类型和约束。
表示数组中的元素都是number类型,如果是其他类型就会报错。
2.7.2.5 对象约束
- minProperties 和 maxProperties :指定对象的最小和最大属性数量。
- additionalProperties :控制是否允许对象中存在未在 properties 中定义的额外属性,默认为True。
示例:校验百度搜索“测试”返回的json数据
additionalProperties :控制是否允许对象中存在未在 properties 中定义的额外属性,默认为True。
比如,此时我手动在json_data中添加一个x字段,该字段就是json_schema中未出现的,因为additionalProperties默认为True,所以添加该字段后可以运行通过。
运行结果:
如果说现在我们不允许添加额外的字段,就可以在json_schema中把additionalproperties设置为False。
运行结果:
注意:additionalProperties放的位置很重要,你把他放在哪,他的作用域就在哪
此时的json_data其实是有两层的,一层是json对象的参数,第二层的"g"字段的参数,如果我们此时在第二层额外添加一个参数,然后将additionalProperties设置在第一层,此时还能起作用吗?
此时运行是通过的,因为additionalProperties只作用在第一层,第二层的additionalProperties还是默认的True。
我们再给第二层也添加上additionalProperties。
运行结果:
minProperties 和 maxProperties :指定对象的最小和最大属性数量。
例如:此时的json对象中只有5个字段:"q"、"p"、"g"、"slid"、"queryid"。
我们可以设置最小有5个字段,最多不超过7个。
此时可以运行成功,那如果再额外添加几个字段,让字段数超过范围呢?
2.7.2.6 必需属性
通过 required 关键字,JSON Schema 可以指定哪些属性是必需的。如果 JSON 实例中缺少这些必需属性,验证将失败。
可能会出现下面的场景:JSON Schema中约束了字段,但是JSON返回值中没有出现该字段,但是用例跑通过的错误情况。
例如此时的json中的"g"字段没有接受到,但是运行结果是正确的。
此时我们就需要设置 required 关键字,表示该字段是必需的。
我们使用JSON转Schema工具时,会自动帮我们生成 required 关键字
演示:
运行结果:
2.7.2.7 依赖关系
dependentRequired 可以定义属性之间的依赖关系。例如,如果某个属性存在,则必须存在另一个属性。
示例:json有如下字段,如果我们设置height依赖于age,如果age存在,那么height也一定存在。
接下来我们分别演示以下几种情况:
1.删除json中的height:
2.删除json中的age
3.删除json中的age和height
结论:age存在时,height必须存在,如果age不存在,则height存不存在都可以。
2.8 logging日志模块
2.8.1 介绍
logging 是 Python 标准库中的一个模块,它提供了灵活的日志记录功能。通过 logging ,开发者可以方便地将日志信息输出到控制台、文件、网络等多种目标,同时支持不同级别的日志记录,以满足不同场景下的需求。
2.8.2 使用
示例1:全局logging
import logging
logging.debug('This is a debug message')
logging.info('This is a info message')
logging.warning('This is a warning message')
logging.error('This is a error message')
logging.critical('This is a critical message')
运行结果:
默认情况下,logging只会打印warning及以上级别的日志。
要想修改默认打印日志级别,例如将级别修改成Info
logging.basicConfig(level=logging.INFO)
示例2:自定义logger并输出到控制台
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("my_logger")
#设置logger的日志级别
logger.setLevel(logging.DEBUG)
logger.debug('This is a debug message')
logger.info('This is a info message')
logger.warning('This is a warning message')
logger.error('This is a error message')
logger.critical('This is a critical message')
虽然全局的logging设置的级别是INFO,但是我们可以自定义一个logger,并且设置对应的级别。在打印时会按照自定义的logger来打印。
示例3:自定义logger并输出到日志文件
logger = logging.getLogger("my_logger")
#设置logger的日志级别
logger.setLevel(logging.DEBUG)
#创建文件处理器,将日志输出到文件中(可以自动创建)
handler = logging.FileHandler("my_logger.log")
#将处理器添加到日志记录器中
logger.addHandler(handler)
logger.debug('This is a debug message')
logger.info('This is a info message')
logger.warning('This is a warning message')
logger.error('This is a error message')
logger.critical('This is a critical message')
- 获取日志记录器: logging.getLogger(__name__) 获取一个日志记录器对象, name 是当前模块的名称。使用模块名称作为日志记录器的名称有助于在大型项目中区分不同模块的目志。
- 设置日志级别: logger.setLevel(logging.DEBUG) 将日志记录器的级别设置为DEBUG ,这意味着所有 DEBUG 及以上级别的日志都会被记录。
- 日志级别金字塔:DEBUG < INFO < WARNING < ERROR < CRITICAL
- 高于设定级别的日志才会被处理
- 创建文件处理器: logging.FileHandler(filename="my_logger.log") 创建一个文件处理器,将日志信息写入到名为 my_logger.log 的文件中。
- 添加处理器: logger.addHandler(handler) 将文件处理器添加到⽇志记录器中,这样日志记录器就会使用这个处理器来处理日志信息。
示例4:设置日志格式
import logging
logger = logging.getLogger("my_logger")
#设置logger的日志级别
logger.setLevel(logging.DEBUG)
#创建文件处理器,将日志输出到文件中(可以自动创建)
handler = logging.FileHandler("my_logger.log")
# 创建⼀个⽇志格式器对象
formatter = logging.Formatter(
"%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s"
)
#将格式器设置到处理器上)
handler.setFormatter(formatter)
#将处理器添加到日志记录器中
logger.addHandler(handler)
logger.debug('This is a debug message')
logger.info('This is a info message')
logger.warning('This is a warning message')
logger.error('This is a error message')
logger.critical('This is a critical message')
运行结果:
logging.Formatter 是用于定义日志输出格式的类。在构造函数中,传递了⼀个格式字符串,用于指定日志信息的格式。格式字符串中使用了一些特殊的占位符(以 % 开头),这些占位符会被替换为相应的日志信息内容
handler.setFormatter(formatter) 将创建的格式器对象设置到处理器上。这意味着处理器在处理日志信息时,会使用这个格式器来格式化日志信息。
通过这种方式,你可以控制日志信息的输出格式,使其包含你感兴趣的信息,如时间戳、日志级别、文件名、函数名、行号等。
2.9 测试报告allure
官方文档:Allure Report Docs – Pytest configuration
2.9.1 介绍与安装
介绍:
Allure Report 由一个框架适配器和 allure 命令行工具组成,是一个流行的开源工具,用于可视化测试运行的结果。它可以以很少甚至零配置的方式添加到您的测试工作流中。它生成的报告可以在任何地方打开,并且任何人都可以阅读,无需深厚的技术知识
安装:
1)下载allure-pytest包
pip install allure-pytest==2.13.5
2)下载Windows版Allure报告
- 下载压缩包:allure下载链接
https://github.com/allure-framework/allure2/releases/download/2.30.0/allure- 2.30.0.zip
- 解压
- 添加系统环境变量
将allure-2.29.0对应bin目录添加到系统环境变量中
- 确认结果
打开cmd,查看allure版本
出现 allure 版本则安装成功。
若出现cmd中执行 allure --version 可以打印版本,但是pycharm控制台执行命令提示命题找不到,则需要修改pycharm中命令行环境,如下:
保存后需要重启pycharm!!!!!!
检查pycharm中命令行是否可以使用allure命令
2.9.2 使用
step1:运行自动化,并指定测试报告放置路径
pytest --alluredir=results_dir(保存测试报告的路径)
⽰例:pytest --alluredir=allure-results
将测试报告保存至allure-resultes文件夹中(不存在则创建)
我们先创建如下两个文件的测试用例
运行命令:
当前项目下自动生成 allre-results 文件夹,存放报告相关文件
清除上一次生成的测试报告:--clean-alluredir
#清除上⼀次⽣成的测试报告
pytest --alluredir=./allure-results --clean-alluredir
例如:现在的allure-results中包含了8个测试报告。
如果我们想让两次生成的测试报告相互之间不影响,那我们就可以使用上面的命令
修改配置文件中的测试报告存放路径:
如果嫌每次敲命令都需要携带测试报告路径比较麻烦,我们可以在配置文件中进行修改。
生成测试报告可以在控制台通过命令将结果保存在 allre-results 文件夹中,也可以在pytest.ini文件中配置测试报告放置路径
addopts = -vs --alluredir allure-results
此时我们只需要输入pytest命令即可。
step2:查看测试报告
1)方法一:启动一个本地服务器来在浏览器中展示测试报告
终端执行命令: allure serve [options] <allure-results> ,自动在浏览器打开测试报告
- --host :指定服务器监听的主机地址,默认为 localhost。
- --port :指定服务器监听的端口号,默认为 0(自动选择空闲端口)
- --clean-alluredir :清除上一次生成的测试报告
一.不指定端口号和主机地址:
#不指定端⼝号和主机地址
allure serve .\allure-results\
通过终端命令最终打开了一个网页版测试报告
Suites会展示所有的测试用例,右侧的界面展示每个界面的执行结果以及运行时间。
二.指定端口号:
#指定端⼝号
allure serve --port 8787 .\allure-results\
指定的端口号必须是空闲的,不能被占用。
2)方法二:从测试结果生成测试报告
终端执行命令: allure generate [options] <allure-results> -o <reports>
- allure-results:测试结果文件夹
- reports:测试报告文件夹
示例:
自动帮我们生成allure-report文件夹
选择其中的index.html,可以选择使用不同的浏览器去访问。
并且打开后,和之前直接在浏览器打开是一样的。
注意:如果历史生成的测试结果json文件不清空,生成的测试报告会整合历史的运行情况,并集成到一个测试报告中。
例如:我们再生成一下测试结果,现在就有8条了,有4条是上一次的,有4条是这一次的。
生成的测试报告如下:在Suites中查看每一条测试用力的Retries,可以看到多了一些内容,这些就是历史的执行结果。
注意:测试报告只能同事存在一份,如果已经在一个文件夹中生成了一份测试报告,再在该文件夹下继续生成就会出错,如下所示:
报错信息中也提示了我们需要使用--clean命令。
allure generate .\allure-results\ -o .\allure-report --clean
此时旧的就被覆盖掉了。