一、什么是测试框架
根据大量的测试实践,抽象出来的常用工具集合,包含大量组件或功能,以及经过验证的方法论。
二、安装Pytest
bash
pip install pytest #安装
pip uninstall pytest #卸载
pip install pytest -U #升级最新版本
pip install pytest==7.2 #升级到指定版本
三、Pytest基础概念
1、创建测试用例
创建用例的规则:
- 创建test_开头文件
- 创建test开头函数
- 创建assert断言
- 不可自定义参数和返回值(pytest中参数有另外的含义)
python
def test_tom():
a=1
b=2
assert a >= b
2、用例发现规则
pytest识别、加载用例的过程,称之为用例发现,具休规则如下:
- 遍历所有的目录(venv和.开头的除外)
- 加载符合要求的py文件(test_开头或者_test结尾)
- 遍历符合要求的类(Test开头且没有_init_)
- 收集符合要求的函数或者方法(test开头)
3、执行测试用例
执行用例核心逻辑是启动pytest框架(pytest会自动收集和执行用例),具体方法有多种:
- 命令行
bash
pytest
1).运行所有
pytest
2).指定模块
pytest -vs tests/server/test_api.py #-vs表示输出详细信息和调试信息,包括print打印的信息
3).指定目录
pytest -vs ./tests/server
4).通过nodeid指定用例运行
pytest -vs ./tests/server/test_api.py::test_api_articles #nodeid由模块名,分隔符(::),类名,方法名,函数名组成
- 代码
python
import pytest
pytest.main() # 启动pytest 测试框架
1).运行所有
pytest.main()
2).指定模块
pytest.main('-vs','tests/server/test_api.py') #-vs表示输出详细信息和调试信息,包括print打印的信息
3).指定目录
pytest.main('-vs','./tests/server')
4).通过nodeid指定用例运行
pytest.main('-vs','tests/server/test_api.py::test_api_articles') #nodeid由模块名,分隔符(::),类名,方法名,函数名组成
4、执行参数详解
-s :表示输出调试信息,包括print打印的信息
-v:显示更详细的信息
-vs :这两个参数可以一起用
-n:支持多线程或者分布式运行测试用例,如果目录下有2个模块,当n=2时会产生2个线程同时执行两个测试模块内容 如: pytest.main ( '一vs', '.tests/server' , '-n=2 ') ; pytest -vs .tests/server -n 2
--reruns num:失败用例重跑,num为重跑次数,如果某个用例失败再跑num次,其他用例正常执行 如:pytest.main ( '一vs', 'tests./server' , '--reruns=2 ') ; pytest -vs .tests/server --reruns 2
-x:表示只要有一个用例报错,那么测试停止
--maxfail=2 表示如果出现两个用例报错,测试就停止
-k:根据测试用例的部分字符串指定测试用例 如:如: pytest.main('-vs','.tests/server','-k=articles') ; pytest -vs .tests/server -k "articles" #只执行测试用例名称中包含"articles"字符串的测试用例
5、理解执行结果
bash
================================================================= test session starts =================================================================
platform win32 -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\vorn\Documents\trae_projects\AutoTest
collected 1 item
test_01.py F [100%]
====================================================================== FAILURES =======================================================================
______________________________________________________________________ test_tom _______________________________________________________________________
def test_tom():
a=1
b=2
> assert a>=b
E assert 1 >= 2
test_01.py:4: AssertionError
=============================================================== short test summary info ===============================================================
FAILED test_01.py::test_tom - assert 1 >= 2
================================================================== 1 failed in 0.19s ==================================================================
pytest执行结果分为几个部分:
- 执行环境:操作系统、python版本、pytest版本
- 执行过程:根目录、用例收集情况(例如上面collected 1 item代表收集到一个pytest用例)、用例名称、用例执行结果、执行进度
- 失败详情:用例名称、用例内容、变量内容、断言提示
- 整体摘要:结果数量、花费时间、失败的文件和用例
pytest场景结果缩写:
| . | passed | 通过 |
|---|---|---|
| F | failed | 失败 |
| E | error | 出错 |
| s | skipped | 跳过 |
| X | xpassed | 意外通过 |
| x | xfailed | 预期失败 |
三、夹具fixture
1、作用
在用例执行之前、执行之后,自动运行代码(准备、请求动作)
场景:
- 之前:启动浏览器;之后:关闭浏览器
- 之前:连接数据库;之后:关闭数据库连接
- 之前:注册账号;之后:删除账号
2、创建fixture
- 创建一个函数(不要test开头)
- 添加装饰器(@pytest.fixture)
- 添加关键字(yield)
python
@pytest.fixture
def func():
#前置操作
yield
#后置操作
3、conftest:跨文件共享fixture
conftest.py:自动发现和加载fixture
conftest支持五级作用域,可以对作用域中所有测试用例执行fixture中的前后置操作:
- function:函数默认值
- class:类
- module:模块(文件)
- package:包(目录)
- session:全局(所有的用例)
4、请求fixture
请求框架调用fixture,并且返回结果:
- 把fixture名字写在用例参数列表(可以接收结果)(推荐的方式)
- 使用@pytest.mark.usefixtures 标记(不可以接收结果)(不推荐)
- 不允许直接调用fixture
5、fixture的作用域
同一个作用域中,fixture不会重复执行;不在同一个作用域中,fixture会重复执行:
- package作用域的fixture,在同一个目录中,不会重复执行
- session作用域的fixture,整个框架运行过程中,只执行1次
示例:
问题1:10个用例,希望使用同一个浏览器?
解答:将fixture的作用域定义为package,10个用例放到同一个目录下。例如打开浏览器执行10个用例,每个用例都依赖前一个用例在浏览器中的数据,全部跑完后关闭浏览器,能够做到跑10个用例只打开一次浏览器就是利用了"同一个作用域中,fixture不会重复执行"的规则。
问题2:希望所有用例执行完毕之后,做某事?
解答:将fixture的作用域定义为session即可。例如执行完所有用例后需要删除注册过的账号。
四、配置文件pytest.ini
pytest框架通过读取pytest.ini配置文件运行
(1)位置:一般放在项目的根目录。
(2)编码:必须是ANSI,可以使用notpad++修改编码格式,并且文件中不可以有中文。
(3)作用:改变pytest默认的行为。
(4)运行的规则:不管是主函数的模式运行,命令行模式运行,都会去读取这个配置文件。
python
[pytest]
addopts = -vs #命令行的参数,用空格分隔
testpaths = ./tests #测试用例的路径
python_files = test_*.py #模块名的规则
python_classes = Test* #类名的规则
python_functions = test #方法名的规则
五、一个Pytest项目示例
项目结构:
python
│ conftest.py
│ main.py
│ pytest.ini
│
└─tests
├─server
│ test_api.py
│
└─web
test_web.py
test_websearch.py
python
import pytest
from selenium import webdriver
@pytest.fixture(scope="package") #scope代表作用域,默认是function(函数)还可以是class(类)、module(包|文件)、package(模块|目录)、session(全局)
def driver():
#启动浏览器
driver=webdriver.Chrome()
driver.get("https://www.sogou.com/")
print("[ok] 打开 sogou.com")
yield driver #把启动好并且随后会关闭的浏览器对象driver传递给测试用例
#关闭浏览器
driver.quit()
print("[ok] 关闭浏览器")
python
import pytest
pytest.main(['-vs'])
#pytest.ini
python
[pytest]
addopts = -vs
testpaths = ./tests
python_files = test_*.py
python_classes = Test*
python_functions = test
#test_api.py
python
import requests
def test_api_articles():
#准备请求要素
method = "get"
url = "https://api.apiopen.top/api/articles/random"
headers = {
"accept": "application/json",
}
#向接口发送请求
response = requests.request(method, url, headers=headers)
print(response.text)
#断言响应结果
assert response.status_code == 200
assert '成功' in response.text
print("[Pass]获取随机文章成功")
def test_api_sentence():
#准备请求要素
method = "get"
url = "https://api.apiopen.top/api/tools/famous-sentence"
headers = {
"accept": "application/json",
}
#向接口发送请求
response = requests.request(method, url, headers=headers)
print(response.text)
#断言响应结果
assert response.status_code == 200
assert '成功' in response.text
print("[Pass]获取随机名言成功")
#test_web.py
python
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
def test_web(driver): #driver是conftest.py中定义的fixture,这里直接使用即可。从python的角度讲就是函数传参;从pytest的角度讲就是请求fixture
#输入搜索内容
search_box=driver.find_element(By.XPATH,'//*[@id="query"]')
search_box.clear()
search_box.send_keys("pytest")
search_box.send_keys(Keys.ENTER) #模拟按下回车键
print("[ok] 输入搜索内容 pytest")
time.sleep(3)
#断言:验证搜索结果包含 pytest
assert "pytest" in driver.page_source
print("[ok] 搜索结果包含 pytest")
time.sleep(3)
#test_websearch.py
python
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
def test_websearch(driver): #driver是conftest.py中定义的fixture,这里直接使用即可。从python的角度讲就是函数传参;从pytest的角度讲就是请求fixture
#点击图片搜索结果
search_box=driver.find_element(By.XPATH,'//*[@id="sogou_pic"]')
search_box.click()
print("[ok] 切换到图片搜索结果")
time.sleep(3)
执行结果:
python
================================ test session starts =================================
platform win32 -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 -- C:\Users\vorn\AppData\Local\Programs\Python\Python314\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\vorn\Documents\trae_projects\AutoTest
configfile: pytest.ini
testpaths: ./tests
collected 4 items
tests/server/test_api.py::test_api_articles {"code":200,"message":"成功","data":{"id":399,"title":"Go 语言入门指南","summary":"详细介绍 Go 语言的基础知识","author":{"account_id":2,"email":"test@example.com","nickname":"testuser","avatar":"https://example.com/avatar.jpg","bio":"这是一段个人简介","gender":"male","age":25,"location":"北京","created_at":"2025-12-01 16:49:51","updated_at":"2026-07-28 10:39:23"},"category":"技术","tags":"Go,编程,后端","cover_image":"https://picsum.photos/800/600?random=1","view_count":0,"like_count":0,"is_published":true,"is_pinned":false,"created_at":"2026-07-06 20:29:16","updated_at":"2026-07-06 20:29:16"}}
[Pass]获取随机文章成功
PASSED
tests/server/test_api.py::test_api_sentence {"code":200,"message":"成功","data":{"name":"早知恁么。悔当初、不把雕鞍锁。","from":"柳永《定风波·自春来》"}}
[Pass]获取随机名言成功
PASSED
tests/web/test_web.py::test_web [ok] 打开 sogou.com
[ok] 输入搜索内容 pytest
[ok] 搜索结果包含 pytest
PASSED
tests/web/test_websearch.py::test_websearch [ok] 切换到图片搜索结果
PASSED[ok] 关闭浏览器
================================= 4 passed in 27.56s =================================

六、数据驱动测试
数据驱动测试 = 参数化测试 + 数据文件
根据数据文件的内容,动态决定用例的数量、内容
数据驱动需要用到装饰器**@pytest.mark.parametrize()**
python
语法:@pytest.mark.parametrize(args_name,args_value)
#args_name 参数名,字符串,可自定义名称
#args_value 参数值(list,tuple,字典列表,字典元组),有多少个值那么测试用例就会执行多少次(以"-"为分隔符计算有多少个值)
#YAML介绍:
yaml是一种数据格式,主要用于配置文件或者编写用例
yaml只有两种数据:
1.键值对
key:(空格)value
2.list
用一个"-"表示一个列表
操作yaml的第三方模块是pyyaml,安装方法:pip install pyyaml
增加数据驱动后的Pytest项目示例:
目录结构:
python
│ conftest.py #保持不变
│ main.py #为了方便演示,只运行test_api.py中的一个测试用例
│ pytest.ini #保持不变
│
├─common #新增公共方法目录
│ yaml_util.py #新增读取数据驱动方法
│
└─tests
├─server
│ test_api.py #method、url配合yaml_util模块做成变量形式
│ test_api_parametrize.yaml #新增数据驱动文件
│
└─web
test_web.py #不使用
test_websearch.py #不使用
python
import pytest
pytest.main(['-vs','tests/server/test_api.py::test_api_articles']) #只运行test_api.py中的一个测试用例test_api_articles
#yaml_util.py
python
import yaml #需要安装模块:pip install pyyaml
import os
#读取数据驱动
def read_testcases(yaml_name):
with open(os.getcwd()+'/tests/server'+'/'+yaml_name,mode='r',encoding='utf-8') as f:
value=yaml.load(stream=f,Loader=yaml.FullLoader)
return value
#test_api.py
python
import pytest
import requests
from common.yaml_util import read_testcases #导入公共函数中"读取数据驱动"方法
@pytest.mark.parametrize("args_name",read_testcases("test_api_parametrize.yaml")) #args_value的值是read_testcases读取yaml中的内容 #args_name返回的是字典类型
def test_api_articles(args_name):
#准备请求要素
method = args_name["request"]["method"]
url = args_name["request"]["url"]
headers = {
"accept": args_name["request"]["headers"]["accept"],
}
#向接口发送请求
response = requests.request(method, url, headers=headers)
print(response.text)
#断言响应结果
assert response.status_code == args_name["validate"]["status_code"]
assert args_name["validate"]["status_text"] in response.text
print("[Pass]获取随机文章成功")
def test_api_sentence():
#准备请求要素
method = "get"
url = "https://api.apiopen.top/api/tools/famous-sentence"
headers = {
"accept": "application/json",
}
#向接口发送请求
response = requests.request(method, url, headers=headers)
print(response.text)
#断言响应结果
assert response.status_code == 200
assert '成功' in response.text
print("[Pass]获取随机名言成功")
#test_api_parametrize.yaml
python
- #这个"-"是每组数据的分隔符,@pytest.mark.parametrize可以根据有几组测试数据就把这个测试用例跑几遍
name: 获取随机文章 #本组数据名称
request: #请求内容
method: get #请求方法
url: https://api.apiopen.top/api/articles/random #请求url
headers: #请求头
accept: application/json
body: #请求体
account: None
password: None
validate: #断言
status_code: 200
status_text: 成功
#name、request、validate是数据驱动最基本的内容,当然这三个基本内容的名称也可自定义
- #第二组数据
name: 获取随机文章 #本组数据名称
request: #请求内容
method: get #请求方法
url: https://api.apiopen.top/api/articles/random #请求url
headers: #请求头
accept: application/json
body: #请求体
account: None
password: None
validate: #断言
status_code: 200
status_text: 成功
执行结果:可以看到,main.py中只执行了一个用例,由于数据驱动中有2组数据,这个用例被跑了2遍,实际项目中可以给出2组不同的数据做不同场景测试。
python
================================ test session starts =================================
platform win32 -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 -- C:\Users\vorn\AppData\Local\Programs\Python\Python314\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\vorn\Documents\trae_projects\AutoTest
configfile: pytest.ini
collected 2 items
tests/server/test_api.py::test_api_articles[args_name0] {"code":200,"message":"成功","data":{"id":410,"title":"Go 语言入门指南","summary":"详细介绍 Go 语言的基础知识","author":{"account_id":2,"email":"test@example.com","nickname":"testuser","avatar":"https://example.com/avatar.jpg","bio":"这是一段个人简介","gender":"male","age":25,"location":"北 京","created_at":"2025-12-01 16:49:51","updated_at":"2026-07-28 10:39:23"},"category":"技术","tags":"Go,编程,后端","cover_image":"https://picsum.photos/800/600?random=1","view_count":0,"like_count":0,"is_published":true,"is_pinned":false,"created_at":"2026-07-06 20:45:41","updated_at":"2026-07-06 20:45:41"}}
[Pass]获取随机文章成功
PASSED
tests/server/test_api.py::test_api_articles[args_name1] {"code":200,"message":"成功","data":{"id":425,"title":"Go 语言入门指南","summary":"详细介绍 Go 语言的基础知识","author":{"account_id":2,"email":"test@example.com","nickname":"testuser","avatar":"https://example.com/avatar.jpg","bio":"这是一段个人简介","gender":"male","age":25,"location":"北 京","created_at":"2025-12-01 16:49:51","updated_at":"2026-07-28 10:39:23"},"category":"技术","tags":"Go,编程,后端","cover_image":"https://picsum.photos/800/600?random=1","view_count":0,"like_count":0,"is_published":true,"is_pinned":false,"created_at":"2026-07-06 20:54:12","updated_at":"2026-07-06 20:54:12"}}
[Pass]获取随机文章成功
PASSED
================================= 2 passed in 0.73s ==================================