
pytest测试框架基础与实战
pytest测试框架基础与实战
前言:pytest,测试框架中的"瑞士军刀"
如果说Python是编程语言中的"人生苦短"代表,那么pytest就是测试框架中的"简洁高效"典范。作为一个用了pytest三年多的老用户,我可以负责任地说:一旦你用上了pytest,就再也回不去unittest那种繁琐的写法了。
今天我们就来深入探讨pytest的各种实用技巧,从基础语法到高级特性,从简单断言到复杂场景,让你彻底掌握这个测试神器。
pytest vs unittest:为什么选择pytest?
在讲具体用法之前,我们先来看看pytest相比于Python自带的unittest有哪些优势:
unittest的痛点
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
self.calc = Calculator()
def test_add(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
def tearDown(self):
# 清理工作
pass
if __name__ == '__main__':
unittest.main()看起来还行?但是当你写了几百个测试用例后,你就会发现:
- 每个测试类都要继承
unittest.TestCase - 断言方法名字又长又难记:
assertEqual、assertIn、assertIsNone... - 测试方法必须以
test_开头 - 需要手动调用
unittest.main()
pytest的优雅
def test_add():
calc = Calculator()
result = calc.add(2, 3)
assert result == 5就这么简单!没有继承,没有复杂的断言方法,就是普通的函数和简单的assert语句。这就是pytest的哲学:让测试变得简单自然。
pytest基础语法
1. 测试用例的编写
pytest的测试用例就是普通的Python函数,只需要遵循几个简单的规则:
# test_basic.py
def test_simple_case():
"""最简单的测试用例"""
assert 1 + 1 == 2
def test_string_operations():
"""字符串操作测试"""
name = "pytest"
assert name.upper() == "PYTEST"
assert len(name) == 6
assert "test" in name
class TestCalculator:
"""测试类的写法"""
def test_add(self):
assert 2 + 3 == 5
def test_subtract(self):
assert 5 - 3 == 2命名规则:
- 测试文件:
test_*.py或*_test.py - 测试函数:
test_*() - 测试类:
Test* - 测试方法:
test_*()
2. 断言的艺术
pytest的断言就是普通的assert语句,但它会提供非常详细的错误信息:
def test_assertions():
# 基本断言
assert 2 + 2 == 4
# 字符串断言
message = "Hello pytest"
assert "pytest" in message
assert message.startswith("Hello")
# 列表断言
numbers = [1, 2, 3, 4, 5]
assert len(numbers) == 5
assert 3 in numbers
# 字典断言
user = {"name": "张三", "age": 25}
assert user["name"] == "张三"
assert "age" in user
# 异常断言
import pytest
with pytest.raises(ZeroDivisionError):
1 / 0当断言失败时,pytest会显示详细的错误信息,比如:
> assert user["name"] == "李四"
E AssertionError: assert '张三' == '李四'
E - 李四
E + 张三这比unittest的AssertionError: '张三' != '李四'要清晰多了。
pytest的核心特性
1. 测试标记(Markers)
标记就像给测试用例贴标签,可以用来分类和筛选测试:
import pytest
@pytest.mark.smoke
def test_login():
"""冒烟测试:登录功能"""
assert True
@pytest.mark.regression
def test_user_profile():
"""回归测试:用户资料"""
assert True
@pytest.mark.slow
def test_data_migration():
"""慢速测试:数据迁移"""
assert True
# 多个标记
@pytest.mark.smoke
@pytest.mark.api
def test_api_health_check():
"""API健康检查"""
assert True运行特定标记的测试:
# 只运行冒烟测试
pytest -m smoke
# 运行冒烟测试和API测试
pytest -m "smoke or api"
# 运行除了慢速测试之外的所有测试
pytest -m "not slow"注册自定义标记(在pytest.ini中):
[tool:pytest]
markers =
smoke: 冒烟测试
regression: 回归测试
slow: 慢速测试
api: API测试
ui: UI测试2. 参数化测试(Parametrize)
参数化测试是pytest的杀手级特性,可以用同一个测试函数测试多组数据:
import pytest
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(1, 1, 2),
(0, 0, 0),
(-1, 1, 0),
])
def test_add(a, b, expected):
"""测试加法运算"""
result = a + b
assert result == expected
# 更复杂的参数化
@pytest.mark.parametrize("username,password,expected_code", [
("admin", "123456", 200),
("user", "password", 200),
("", "123456", 400),
("admin", "", 400),
("invalid", "wrong", 401),
])
def test_login_api(username, password, expected_code):
"""测试登录API"""
response = login(username, password)
assert response.status_code == expected_code从外部文件读取测试数据:
import json
import pytest
def load_test_data():
"""从JSON文件加载测试数据"""
with open("test_data.json", "r") as f:
return json.load(f)
@pytest.mark.parametrize("test_case", load_test_data())
def test_api_with_external_data(test_case):
"""使用外部数据进行测试"""
response = call_api(test_case["input"])
assert response.status_code == test_case["expected_status"]
assert response.json() == test_case["expected_response"]3. 固件(Fixtures)
Fixtures是pytest中用于测试前置和后置处理的机制,比unittest的setUp/tearDown更加灵活:
import pytest
@pytest.fixture
def user_data():
"""提供测试用户数据"""
return {
"username": "testuser",
"email": "test@example.com",
"age": 25
}
@pytest.fixture
def database_connection():
"""数据库连接fixture"""
# 前置:建立连接
conn = create_database_connection()
yield conn # 这里是测试用例执行的地方
# 后置:关闭连接
conn.close()
def test_user_creation(user_data, database_connection):
"""测试用户创建"""
user_id = create_user(database_connection, user_data)
assert user_id is not None
# 验证用户是否真的创建了
user = get_user(database_connection, user_id)
assert user["username"] == user_data["username"]Fixture的作用域:
@pytest.fixture(scope="function") # 每个测试函数执行一次(默认)
def function_fixture():
return "function"
@pytest.fixture(scope="class") # 每个测试类执行一次
def class_fixture():
return "class"
@pytest.fixture(scope="module") # 每个模块执行一次
def module_fixture():
return "module"
@pytest.fixture(scope="session") # 整个测试会话执行一次
def session_fixture():
return "session"自动使用的Fixture:
@pytest.fixture(autouse=True)
def setup_test_environment():
"""每个测试前自动执行"""
print("设置测试环境")
yield
print("清理测试环境")实战案例:接口测试框架
让我们用一个完整的例子来展示pytest在接口测试中的应用:
# conftest.py - 全局配置和fixtures
import pytest
import requests
@pytest.fixture(scope="session")
def api_client():
"""API客户端fixture"""
class APIClient:
def __init__(self):
self.base_url = "https://api.example.com"
self.session = requests.Session()
self.token = None
def login(self, username, password):
response = self.session.post(
f"{self.base_url}/login",
json={"username": username, "password": password}
)
if response.status_code == 200:
self.token = response.json()["token"]
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
return response
def get(self, endpoint):
return self.session.get(f"{self.base_url}{endpoint}")
def post(self, endpoint, data):
return self.session.post(f"{self.base_url}{endpoint}", json=data)
return APIClient()
@pytest.fixture
def authenticated_client(api_client):
"""已认证的客户端"""
api_client.login("admin", "password")
return api_client# test_user_api.py - 用户API测试
import pytest
class TestUserAPI:
"""用户API测试套件"""
@pytest.mark.smoke
def test_user_login_success(self, api_client):
"""测试用户登录成功"""
response = api_client.login("admin", "password")
assert response.status_code == 200
assert "token" in response.json()
@pytest.mark.parametrize("username,password,expected_status", [
("", "password", 400),
("admin", "", 400),
("invalid", "wrong", 401),
])
def test_user_login_failure(self, api_client, username, password, expected_status):
"""测试用户登录失败场景"""
response = api_client.login(username, password)
assert response.status_code == expected_status
def test_get_user_profile(self, authenticated_client):
"""测试获取用户资料"""
response = authenticated_client.get("/user/profile")
assert response.status_code == 200
profile = response.json()
assert "username" in profile
assert "email" in profile
@pytest.mark.slow
def test_user_data_export(self, authenticated_client):
"""测试用户数据导出(慢速测试)"""
response = authenticated_client.post("/user/export", {"format": "csv"})
assert response.status_code == 200
assert response.headers["Content-Type"] == "text/csv"pytest插件生态
pytest有丰富的插件生态,可以大大扩展其功能:
1. pytest-html:生成HTML报告
pip install pytest-html
# 生成HTML报告
pytest --html=report.html --self-contained-html2. pytest-xdist:并行执行
pip install pytest-xdist
# 使用4个进程并行执行
pytest -n 43. pytest-rerunfailures:失败重试
pip install pytest-rerunfailures
# 失败的测试重试2次
pytest --reruns 2 --reruns-delay 14. pytest-mock:Mock支持
pip install pytest-mock
# 在测试中使用
def test_api_call(mocker):
mock_requests = mocker.patch('requests.get')
mock_requests.return_value.status_code = 200
result = call_external_api()
assert result is not None最佳实践与技巧
1. 测试组织结构
tests/
├── conftest.py # 全局配置和fixtures
├── test_auth/ # 认证相关测试
│ ├── test_login.py
│ └── test_logout.py
├── test_user/ # 用户相关测试
│ ├── test_profile.py
│ └── test_settings.py
└── utils/ # 测试工具
├── helpers.py
└── data_factory.py2. 配置管理
# pytest.ini
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --strict-markers
markers =
smoke: 冒烟测试
regression: 回归测试
slow: 慢速测试3. 测试数据工厂
# utils/data_factory.py
import random
from faker import Faker
fake = Faker('zh_CN')
class UserFactory:
@staticmethod
def create_user_data():
return {
"username": fake.user_name(),
"email": fake.email(),
"phone": fake.phone_number(),
"age": random.randint(18, 65)
}
@staticmethod
def create_batch_users(count=10):
return [UserFactory.create_user_data() for _ in range(count)]总结
pytest就像一把锋利的瑞士军刀,功能强大而使用简单。掌握了这些基础知识和实战技巧,你就可以构建出高效、可维护的测试套件。
关键要点回顾:
- 简洁的语法:普通函数 + assert语句
- 灵活的标记:用于测试分类和筛选
- 强大的参数化:一个函数测试多组数据
- 优雅的Fixtures:处理测试前置和后置
- 丰富的插件:扩展pytest功能
下一篇文章,我们将深入学习requests库的高级用法和封装技巧,敬请期待!
