目录:
pytest 是一个 Python 测试框架,用于测试代码,支持单元测试、集成测试、功能测试等。
安装依赖
$ pip3 install pytest
文件名:文件名应以 test_ 开头或以 _test 结尾。例如 test_example.py 或 example_test.py;
测试类:类名应以 Test 开头(例如 TestExample),且类中不应包含 __init__ 方法;
测试函数:函数名或方法名应以 test_ 开头(例如 test_addition)。不符合此命名模式的函数将不会被执行;
运行所有用例:在项目根目录直接执行 pytest;
运行指定目录:pytest tests/;
运行指定文件:pytest test_sample.py;
运行指定类:pytest test_demo2.py::TestDemo;
运行指定方法:pytest test_demo2.py::TestDemo::test_demo1;
在代码中运行:可以在脚本中调用 pytest.main() 来执行:
if __name__ == '__main__':
pytest.main(["-s", "test_sample.py"]) # -s 用于显示打印输出
其中:pytest 命令有一些常见参数,介绍如下:
-v:显示详细的测试信息,包括每个用例的名称和结果;-s:显示测试用例中 print 等语句的输出信息;-k:只运行名称中包含指定关键词的用例,如 pytest -k “demo”;-m:运行被特定标记(mark)装饰的用例,需要先在 pytest.ini 配置文件中注册标记# test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5 # 此断言会失败
在包含 test_sample.py 文件的目录下执行 pytest 命令。
$ pytest
================================= test session starts ==================================
platform darwin -- Python 3.11.1, pytest-8.3.3, pluggy-1.5.0
sensitiveurl: .*
rootdir: /Users/dkvirus/Documents
plugins: variables-3.1.0, html-4.1.1, metadata-3.1.1, allure-pytest-2.13.5, ordering-0.6, rerunfailures-15.0, base-url-2.1.0, anyio-3.6.2, xdist-3.6.1, selenium-4.1.0
collected 1 item
test_sample.py F [100%]
======================================= FAILURES =======================================
_____________________________________ test_answer ______________________________________
def test_answer():
> assert inc(3) == 5 # 此断言会失败
E assert 4 == 5
E + where 4 = inc(3)
test_sample.py:5: AssertionError
=============================== short test summary info ================================
FAILED test_sample.py::test_answer - assert 4 == 5
================================== 1 failed in 0.06s ===================================
输出信息中,一个点 . 代表一个测试通过,F 代表失败,并会显示详细的断言错误信息。
class TestDemo:
def test_demo1(self):
assert 11 == 11 # 通过
def test_demo2(self):
assert 22 == 21 # 失败
运行 pytest 后,会清晰展示类中每个测试方法的结果。
$ pytest
================================= test session starts ==================================
platform darwin -- Python 3.11.1, pytest-8.3.3, pluggy-1.5.0
sensitiveurl: .*
rootdir: /Users/dkvirus/Documents/vscode-docs
plugins: variables-3.1.0, html-4.1.1, metadata-3.1.1, allure-pytest-2.13.5, ordering-0.6, rerunfailures-15.0, base-url-2.1.0, anyio-3.6.2, xdist-3.6.1, selenium-4.1.0
collected 2 items
test_sample.py .F [100%]
======================================= FAILURES =======================================
_________________________________ TestDemo.test_demo2 __________________________________
self = <test_sample.TestDemo object at 0x107aa5410>
def test_demo2(self):
> assert 22 == 21 # 失败
E assert 22 == 21
test_sample.py:6: AssertionError
=============================== short test summary info ================================
FAILED test_sample.py::TestDemo::test_demo2 - assert 22 == 21
============================= 1 failed, 1 passed in 0.06s ==============================
输出信息最后一行,可以看到 1failed, 1 passed 表示类中的两个方法,一个测试通过,一个测试失败。
@pytest.mark.parametrize,参数化可以避免为多组输入输出数据编写重复的测试代码,极大提高效率。
import pytest
@pytest.mark.parametrize(
"test_input, expected",
[("3+5", 8), ("'2'+'4'", "24"), ("6*9", 54)]
)
def test_eval(test_input, expected):
assert eval(test_input) == expected
执行时,test_eval 函数会分别使用三组参数各运行一次。
@pytest.fixture 通过 yield 语句将测试执行过程分为 “前置” 和 “后置” 两部分,前置代码在测试用例执行前运行,用于准备测试环境;后置代码在测试用例执行后运行,用于清理资源。
import pytest
import tempfile
import os
@pytest.fixture
def temp_file():
# 前置:创建临时文件
file = tempfile.NamedTemporaryFile(mode='w', delete=False)
file.write("测试数据")
file.close()
yield file.name # 将文件名传递给测试用例
# 后置:清理临时文件
os.unlink(file.name)
def test_file_operations(temp_file):
"""测试文件读取操作"""
with open(temp_file, 'r') as f:
content = f.read()
assert content == "测试数据"
print(f"测试文件:{temp_file}")
在这个示例中:
当多个测试文件需要共享相同的 Fixture 时,可以将其放在 conftest.py 文件中,pytest 会自动发现并加载这个文件中的 Fixture。
项目目录结构如下:
project/
├── conftest.py # 共享 Fixture 定义,文件名不能改变
├── test_api.py # API测试
└── test_db.py # 数据库测试
conftest.py 内容如下:
import pytest
import requests
@pytest.fixture
def api_session():
"""创建共享的HTTP会话"""
session = requests.Session()
# 可以在这里设置通用请求头
session.headers.update({'User-Agent': 'pytest-test-client'})
yield session
# 测试结束后关闭会话
session.close()
@pytest.fixture(scope='module')
def db_connection():
"""模块级别的数据库连接"""
import sqlite3
conn = sqlite3.connect(':memory:')
# 创建测试表
conn.execute('''CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)''')
conn.execute("INSERT INTO users (name) VALUES ('测试用户')")
conn.commit()
yield conn
conn.close()
test_api.py 中使用共享 Fixture:
def test_get_user(api_session):
"""测试获取用户信息"""
# 使用共享的api_session,无需重复创建
response = api_session.get(' https://api.example.com/users')
assert response.status_code == 200
def test_create_user(api_session):
"""测试创建用户"""
data = {'name': '新用户'}
response = api_session.post(' https://api.example.com/users', json=data)
assert response.status_code == 201
test_db.py 中使用共享 Fixture:
def test_user_query(db_connection):
"""测试数据库查询"""
cursor = db_connection.cursor()
cursor.execute("SELECT * FROM users WHERE name = '测试用户'")
result = cursor.fetchone()
assert result is not None
assert result[1] == '测试用户'
Fixture 可以通过 scope 参数指定作用域,默认为 ‘function’。
每个测试函数都会重新创建 Fixture,适合需要完全隔离的测试环境。
@pytest.fixture(scope='function')
def fresh_user():
"""每个测试函数都创建一个新用户"""
user = {'id': 1, 'name': '临时用户', 'score': 0}
yield user
print(f"清理用户:{user['name']}")
def test_user_score_1(fresh_user):
fresh_user['score'] += 10
assert fresh_user['score'] == 10
def test_user_score_2(fresh_user):
# 这是一个全新的用户对象,score为初始值0
fresh_user['score'] += 20
assert fresh_user['score'] == 20
同一个测试类中的所有测试方法共享同一个 Fixture 实例。
@pytest.fixture(scope='class')
class TestCalculator:
@pytest.fixture(scope='class')
def calculator(self):
"""整个测试类共享一个计算器实例"""
calc = Calculator()
print("创建计算器实例")
yield calc
print("清理计算器实例")
def test_addition(self, calculator):
result = calculator.add(2, 3)
assert result == 5
def test_subtraction(self, calculator):
# 使用同一个calculator实例
result = calculator.subtract(5, 2)
assert result == 3
同一个模块(文件)中的所有测试共享 Fixture。
# test_module.py
@pytest.fixture(scope='module')
def config_data():
"""整个模块共享配置数据"""
config = load_config('test_config.json')
print("加载模块配置")
yield config
print("清理模块配置")
def test_feature_a(config_data):
assert config_data['feature_a'] is True
def test_feature_b(config_data):
assert config_data['feature_b_enabled'] is False
整个测试会话(一次 pytest 执行)期间只创建一次 Fixture。
# conftest.py
@pytest.fixture(scope='session')
def global_config():
"""全局配置,整个测试会话只加载一次"""
config = {
'api_base_url': 'https://api.example.com',
'timeout': 30,
'max_retries': 3
}
print("=== 加载全局配置 ===")
yield config
print("=== 清理全局配置 ===")
Fixture 可以通过 params 参数实现参数化,为同一个测试逻辑提供多组测试数据。
import pytest
@pytest.fixture(params=[
{'username': 'user1', 'password': 'pass123', 'expected': True},
{'username': 'user2', 'password': 'wrong', 'expected': False},
{'username': '', 'password': 'pass123', 'expected': False},
{'username': 'user3', 'password': '', 'expected': False}
])
def login_test_data(request):
"""参数化的登录测试数据"""
data = request.param
yield data
def test_login(login_test_data):
"""使用参数化数据测试登录功能"""
result = login(
username=login_test_data['username'],
password=login_test_data['password']
)
assert result == login_test_data['expected']
print(f"测试:{login_test_data['username']} -> {'成功' if result else '失败'}")
运行这个测试时,pytest 会自动执行 4 次 test_login,每次使用不同的测试数据。
Fixture 可以通过 autouse=True 参数实现自动应用。
import pytest
import logging
import time
@pytest.fixture(autouse=True)
def setup_logging():
"""自动设置日志,所有测试都会应用"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
print("=== 测试开始,日志已配置 ===")
@pytest.fixture(autouse=True, scope='function')
def test_timer():
"""自动计时每个测试的执行时间"""
start_time = time.time()
yield
end_time = time.time()
duration = end_time - start_time
print(f"测试执行时间: {duration:.3f}秒")
def test_quick_operation():
time.sleep(0.1)
assert True
def test_slow_operation():
time.sleep(0.5)
assert True
执行 pytest -s 可以看到打印了每个测试案例的执行时间。
Fixture 可以依赖其他 Fixture,形成嵌套结构,用于构建复杂的测试环境。
import pytest
@pytest.fixture
def database():
"""基础数据库连接"""
db = Database('test.db')
db.connect()
yield db
db.disconnect()
@pytest.fixture
def empty_table(database):
"""清空用户表"""
database.execute("DELETE FROM users")
yield database
# 测试后不需要特殊清理
@pytest.fixture
def sample_users(database):
"""预置样本用户数据"""
users = [
{'id': 1, 'name': 'Alice', 'age': 25},
{'id': 2, 'name': 'Bob', 'age': 30},
{'id': 3, 'name': 'Charlie', 'age': 35}
]
for user in users:
database.insert('users', user)
yield database
database.execute("DELETE FROM users")
def test_empty_database(empty_table):
"""测试空数据库"""
count = empty_table.count('users')
assert count == 0
def test_sample_data(sample_users):
"""测试有样本数据的数据库"""
count = sample_users.count('users')
assert count == 3
user = sample_users.query('users', id=2)
assert user['name'] == 'Bob'
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
@pytest.fixture(scope='session')
def browser():
"""全局浏览器实例"""
driver = webdriver.Chrome()
driver.implicitly_wait(10)
driver.maximize_window()
yield driver
driver.quit()
@pytest.fixture
def logged_in_user(browser):
"""已登录的用户会话"""
# 访问登录页面
browser.get(' https://example.com/login')
# 输入凭据
browser.find_element(By.ID, 'username').send_keys('testuser')
browser.find_element(By.ID, 'password').send_keys('testpass')
browser.find_element(By.ID, 'login-btn').click()
# 验证登录成功
assert 'Dashboard' in browser.title
yield browser
# 登出
browser.find_element(By.ID, 'logout-btn').click()
def test_user_profile(logged_in_user):
"""测试用户个人资料页面"""
browser = logged_in_user
browser.get(' https://example.com/profile')
# 验证页面元素
username = browser.find_element(By.CLASS_NAME, 'username').text
assert username == 'testuser'
# 测试个人资料更新
browser.find_element(By.ID, 'edit-profile').click()
# ... 更多测试操作
import pytest
import requests
@pytest.fixture
def auth_token():
"""获取认证令牌"""
response = requests.post(
' https://api.example.com/auth/login',
json={'username': 'test', 'password': 'test123'}
)
assert response.status_code == 200
token = response.json()['token']
yield token
# 注销
requests.post(
' https://api.example.com/auth/logout',
headers={'Authorization': f'Bearer {token}'}
)
@pytest.fixture
def authenticated_session(auth_token):
"""带认证的请求会话"""
session = requests.Session()
session.headers.update({
'Authorization': f'Bearer {auth_token}',
'Content-Type': 'application/json'
})
yield session
session.close()
@pytest.fixture
def test_user(authenticated_session):
"""创建测试用户"""
user_data = {
'name': 'Fixture测试用户',
'email': 'fixture_test@example.com',
'role': 'tester'
}
response = authenticated_session.post(
' https://api.example.com/users',
json=user_data
)
assert response.status_code == 201
user_id = response.json()['id']
yield user_id
# 清理:删除测试用户
authenticated_session.delete(f' https://api.example.com/users/ {user_id}')
def test_user_operations(test_user, authenticated_session):
"""测试用户相关操作"""
user_id = test_user
# 1. 获取用户信息
response = authenticated_session.get(
f' https://api.example.com/users/ {user_id}'
)
assert response.status_code == 200
user_info = response.json()
assert user_info['email'] == 'fixture_test@example.com'
# 2. 更新用户信息
update_data = {'name': '更新后的名称'}
response = authenticated_session.patch(
f' https://api.example.com/users/ {user_id}',
json=update_data
)
assert response.status_code == 200
# 3. 验证更新
response = authenticated_session.get(
f' https://api.example.com/users/ {user_id}'
)
assert response.json()['name'] == '更新后的名称'
安装依赖
$ pip3 install pytest-html
在执行测试时添加 –html 参数:
$ pytest --html=report.html
↶ 返回首页 ↶