Pytest
1. requirements.txt
bash
# requirements.txt
# pip install -r requirements.txt
allure-pytest
pytest-html
pytest-rerunfailures
pytest-xdist
pytest-ordering
pytest
PyYAML
requests
jsonpath-ng
selenium
2. pytest.ini
ini
# pytest.ini
[pytest]
addopts = -vs --alluredir ./temp --clean-alluredir
# 然后手动执行
# allure generate ./temp -o ./allure-report --clean
# 或者:这会在 /var/folder 临时目录下新建的,每次执行serve都会新建的
# allure serve ./report
testpaths = testApi
python_files = test_*.py
python_classes = Test*
python_functions = test
markers =
smoke: test environment
uat:uat environment
# log日志配置
log_cli = true
log_cli_level = info
log_cli_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format = %Y-%m-%d %H:%M:%S
; log_file = ./logs/test.log # 在conftest.py文件中重新设置
log_file_level = info
log_file_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s)
log_file_date_format = %Y-%m-%d %H:%M:%S
3. conftest.py
bash
# conftest.py
# log_file = ./logs/test.log 已经存在pytest.ini中
# 在 conftest.py 里面重新定义
def pytest_configure(config):
# 配置 pytest设置日志文件名
current_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
log_file = f"./logs/{current_time}.log"
config.option.log_file = log_file
4. 命令行参数
bash
# 增加测试运行的冗长程度,显示每个测试函数的名称和结果
-v, --verbose
# 减少测试运行的冗长程度
-q, --quiet
# 仅运行与表达式匹配的测试。表达式可以是测试函数名称的一部分
-k EXPRESSION
# 禁止显示警告信息
--disable-warnings
# 遇到第一个错误或失败就停止测试
-x, --exitfirst:
# 当失败的测试达到指定数量时停止运行
--maxfail=num
# 不捕获测试中的输出(例如print语句的输出)`-s`, `--capture=no`:
# 忽略指定路径下的测试文件
--ignore=path
# 显示N个最慢的测试用例和它们的执行时间
--durations=N
# 生成一个HTML格式的测试报告(需要安装`pytest-html`插件)
--html=report.html
# 并行运行测试,需要安装pytest-xdist插件
-n NUM
# 将运行所有用装饰器修饰的测试@pytest.mark.smoke
pytest -m smoke
# 查看缓存的内容
pytest --cache-show
# 采用可选参数来指定用于过滤的 glob 模式
pytest --cache-show example/*
# 清除所有缓存文件和值
pytest --cache-clear
5. setup and teardown
py
# Module-level
# 在整个模块开始前执行一次
def setup_module():
print("setup_module")
# 在整个模块的所有测试执行完后执行一次
def teardown_module():
print("teardown_module")
def test_example1():
print("Executing test_example1")
def test_example2():
print("Executing test_example2")
py
# Class-level
class TestExample:
@classmethod
def setup_class(cls):
"""在类的所有测试开始前执行一次"""
print("Class setup")
@classmethod
def teardown_class(cls):
"""在类的所有测试执行完后执行一次"""
print("Class teardown")
def test_method1(self):
print("Executing test_method1")
def test_method2(self):
print("Executing test_method2")
py
# Method-level
# setup_method和teardown_method:针对类中的每个测试方法
class TestExampleMethod:
def setup_method(self, method):
"""在每个测试方法执行前执行"""
print("Method setup for", method.__name__)
def teardown_method(self, method):
"""在每个测试方法执行后执行"""
print("Method teardown for", method.__name__)
def test_example1(self):
print("Executing test_example1")
def test_example2(self):
print("Executing test_example2")
py
# Function-level
# setup_function和teardown_function:针对模块级别的独立的测试函数(类里的方法不生效)
def setup_function():
# 每个测试函数执行前调用
print("setup_function called")
def teardown_function():
# 每个测试函数执行后调用
print("teardown_function called")
def test_one():
print("Test 1 executing")
def test_two():
print("Test 2 executing")
6. fixture
scope
:(function(default),class,module,session,package)
py
# Function Scope
import pytest
@pytest.fixture(scope='function')
def function_scope_fixture():
print("\nSetup for function scope fixture")
yield
print("\nTeardown for function scope fixture")
def test_function_scope_1(function_scope_fixture):
print("Running test 1 with function scope fixture")
def test_function_scope_2(function_scope_fixture):
print("Running test 2 with function scope fixture")
py
# Class Scope
import pytest
@pytest.fixture(scope='class')
def class_scope_fixture():
print("\nSetup for class scope fixture")
yield
print("\nTeardown for class scope fixture")
class TestClassScope:
def test_class_scope_1(self, class_scope_fixture):
print("Running test 1 with class scope fixture")
def test_class_scope_2(self, class_scope_fixture):
print("Running test 2 with class scope fixture")
py
# Module Scope
import pytest
@pytest.fixture(scope='module')
def module_scope_fixture():
print("\nSetup for module scope fixture")
yield
print("\nTeardown for module scope fixture")
def test_module_scope_1(module_scope_fixture):
print("Running test 1 with module scope fixture")
def test_module_scope_2(module_scope_fixture):
print("Running test 2 with module scope fixture")
print("Running test 2 with class scope fixture")
py
# Session Scope
import pytest
@pytest.fixture(scope='session')
def session_scope_fixture():
print("\nSetup for session scope fixture")
yield
print("\nTeardown for session scope fixture")
def test_session_scope_1(session_scope_fixture):
print("Running test 1 with session scope fixture")
def test_session_scope_2(session_scope_fixture):
print("Running test 2 with session scope fixture")
params
: 用于参数化 fixture,使得每个测试函数可以使用不同的参数集
py
import pytest
@pytest.fixture(params=[1, 2, 3])
def param_fixture(request):
return request.param
def test_param_fixture(param_fixture):
print(f"Running test with param {param_fixture}")
# 在这个示例中,test_param_fixture 将运行三次,分别使用参数 1, 2, 3
autouse
: 用于自动使用 fixture 而不需要在测试函数中显式地传递
py
import pytest
@pytest.fixture(autouse=True)
def autouse_fixture():
print("\nSetup for autouse fixture")
yield
print("\nTeardown for autouse fixture")
def test_autouse_1():
print("Running test 1 with autouse fixture")
def test_autouse_2():
print("Running test 2 with autouse fixture")
ids
: 用于为参数化的 fixture 提供自定义的 ID,便于在测试报告中识别
py
import pytest
@pytest.fixture(params=[1, 2, 3], ids=["one", "two", "three"])
def id_fixture(request):
return request.param
def test_id_fixture(id_fixture):
print(f"Running test with param {id_fixture}")
# 测试报告中将显示自定义的 ID one, two, 和 three,而不是原始参数值 1, 2, 和 3
name
:用于重命名 fixture
py
import pytest
@pytest.fixture(name='custom_name_fixture')
def original_name_fixture():
return "fixture value"
def test_custom_name(custom_name_fixture):
assert custom_name_fixture == "fixture value"
7. parametrize
@pytest.mark.parametrize 装饰器允许你为测试函数提供多个参数集。它接收两个主要参数
- 参数名的字符串,多个参数名用逗号分隔
- 参数值的列表或列表的列表,每个列表元素是一个参数集
py
import pytest
@pytest.mark.parametrize("input, expected", [(1, 2),(2, 3),(3, 4),])
def test(input, expected):
assert input + 1 == expected
# 在这个示例中,test函数将运行三次,分别使用参数 (1, 2),(2, 3),(3, 4)
参数组合
py
# 参数组合
import pytest
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test(x, y):
print(f"Running test with x={x} and y={y}")
# test 函数将运行四次,分别使用 (1, 10),(1, 20),(2, 10),(2, 20)
自定义参数 ID
py
# 自定义参数 ID
import pytest
@pytest.mark.parametrize("input, expected", [
(1, 2),
(2, 3),
(3, 4),
], ids=["one_plus_one", "two_plus_one", "three_plus_one"])
def test_increment_with_ids(input, expected):
assert input + 1 == expected
#在这个示例中,测试报告中将显示自定义的ID one_plus_one,two_plus_one,three_plus_one
使用生成器参数化
py
# 使用生成器参数化
import pytest
def generate_params():
for i in range(5):
yield (i, i + 1)
@pytest.mark.parametrize("input, expected", generate_params())
def test_with_generator(input, expected):
assert input + 1 == expected
# test_with_generator 函数将运行五次,分别使用生成器生成的参数集
参数化与fixture结合
py
# 参数化与fixture结合
import pytest
@pytest.fixture
def base_value():
return 10
@pytest.mark.parametrize("multiplier", [1, 2, 3])
def test_with_fixture_and_param(base_value, multiplier):
result = base_value * multiplier
assert result in [10, 20, 30]
# 在这个示例中,test_with_fixture_and_param 函数将运行三次
# 分别使用 multiplier 的值 1,2,3,并且每次都使用 base_value fixture 的返回值 10
8. 缓存 session 数据
使用 request.config.cache 来缓存 session 数据
py
# conftest.py
import pytest
import requests
from jsonpath_ng import parse
from requests import RequestException
def login():
session = requests.Session()
url = 'http://localhost:3000/login/cellphone'
params = {
'phone': '15000840699',
'password': '**********'
}
try:
res = session.get(url, params=params)
res.raise_for_status()
except RequestException as e:
pytest.fail(f"请求登录接口失败: {e}")
jsonpath_expressions_cookie = parse('$.cookie')
jsonpath_expressions_token = parse('$.token')
match_cookie = jsonpath_expressions_cookie.find(res.json())
match_token = jsonpath_expressions_token.find(res.json())
if match_cookie and match_token:
token = match_token[0].value
cookie = match_cookie[0].value
session.cookies.set('Set-Cookie', cookie)
session.headers['Authorization'] = f'Bearer {token}'
return session
@pytest.fixture(scope='session')
def session(request):
cache = request.config.cache
# 尝试从缓存中加载session数据
cached_cookies = cache.get("session_cookies", default=None)
cached_token = cache.get("session_token", default=None)
if cached_cookies is None or cached_token is None:
# 如果缓存中没有session数据,则重新登录并缓存
session = login()
cookies_list = [{'name': c.name, 'value': c.value, 'domain': c.domain, 'path': c.path} for c in session.cookies]
token = session.headers.get('Authorization')
cache.set("session_cookies", cookies_list)
cache.set("session_token", token)
else:
# 如果缓存中有session数据,则使用缓存的数据
session = requests.Session()
for cookie in cached_cookies:
session.cookies.set(cookie['name'], cookie['value'], domain=cookie['domain'], path=cookie['path'])
session.headers['Authorization'] = f'Bearer {cached_token}'
print('---------session.headers--------->', session.headers)
print('---------session.cookies--------->', session.cookies)
yield session
py
# test_user.py
import unittest
import pytest
@pytest.mark.usefixtures('session')
class TestUser:
def test_user_account(self, session):
url = 'http://localhost:3000/user/account'
response = session.get(url)
def test_user_status(self, session):
url = 'http://localhost:3000/login/status'
response = session.get(url)
def test_user_level(self, session):
url = 'http://localhost:3000/user/level'
response = session.get(url)
py
# test_playlist.py
import pytest
@pytest.mark.usefixtures('session')
class TestPlaylist:
def test_playlist_catlist(self, session):
url = 'http://localhost:3000/playlist/catlist'
response = session.get(url)
assert response.status_code == 200
def test_playlist_hot(self, session):
url = 'http://localhost:3000/playlist/hot'
response = session.get(url)
assert response.status_code == 200