跳到主要内容

Pytest

简介

Pytest 是一个功能强大且易于使用的 Python 测试框架,支持简单的单元测试到复杂的功能测试。它具有以下特点:

  • 简洁的测试语法,使用 assert 语句即可
  • 强大的 Fixture 系统用于测试依赖管理
  • 丰富的插件生态系统
  • 详细的测试报告和失败信息
  • 支持参数化测试、标记测试、并行测试等高级特性
  • 兼容 unittest 和 nose 测试

快速开始

安装

# 基础安装
pip install pytest

# 安装常用插件
pip install pytest-cov pytest-xdist pytest-mock pytest-asyncio

第一个测试

创建测试文件 test_sample.py

# test_sample.py

def test_addition():
assert 1 + 1 == 2

def test_string_concatenation():
assert "hello" + " " + "world" == "hello world"

def test_division():
result = 10 / 2
assert result == 5.0
assert isinstance(result, float)

运行测试:

# 运行所有测试
pytest

# 运行指定文件
pytest test_sample.py

# 运行指定测试函数
pytest test_sample.py::test_addition

# 显示详细输出
pytest -v

# 显示打印输出
pytest -s

测试发现

命名规则

Pytest 自动发现符合以下规则的测试:

# 文件命名规则
# test_*.py
# *_test.py

# 测试函数/方法命名规则
# test_*

# 示例文件结构
tests/
├── test_user.py # 会被发现
├── auth_test.py # 会被发现
├── helper.py # 不会被发现
└── conftest.py # 特殊文件,fixture 定义

测试收集

# test_collection.py

class TestUser:
"""测试类:以 Test 开头的类会被自动发现"""

def test_create_user(self):
"""测试方法:以 test_ 开头"""
assert True

def normal_method(self):
"""普通方法:不会被当作测试"""
pass

def test_login():
"""独立测试函数"""
assert True

自定义测试收集规则:

# 只收集特定模式文件
pytest -k "test_"

# 排除特定目录
pytest --ignore=tests/integration/

# 只收集特定目录
pytest tests/unit/

断言

assert语句

Pytest 使用 Python 原生的 assert 语句,无需导入特殊断言方法:

# test_assertions.py

def test_equality():
assert 1 == 1
assert "hello" == "hello"

def test_comparison():
assert 5 > 3
assert 10 >= 10
assert 2 < 4

def test_membership():
assert 3 in [1, 2, 3, 4]
assert "key" in {"key": "value"}

def test_identity():
obj = object()
assert obj is obj

def test_boolean():
assert bool(1) is True
assert bool(0) is False
assert not None

详细断言信息:

def test_dict_comparison():
expected = {
"name": "Alice",
"age": 30,
"city": "New York"
}
actual = {
"name": "Alice",
"age": 25, # 不同
"city": "New York"
}
assert expected == actual
# Pytest 会显示详细的差异信息

异常断言

使用 pytest.raises 测试异常:

import pytest

def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0

def test_key_error():
with pytest.raises(KeyError):
{}["missing_key"]

def test_exception_message():
with pytest.raises(ValueError, match="must be positive"):
validate_age(-1)

def validate_age(age):
if age < 0:
raise ValueError("Age must be positive")
return age

def test_exception_attributes():
with pytest.raises(ValueError) as exc_info:
validate_age(-1)

assert exc_info.type is ValueError
assert str(exc_info.value) == "Age must be positive"
assert "positive" in str(exc_info.value)

警告断言

import warnings

def test_warning():
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")

# 触发警告
warnings.warn("This is a warning", UserWarning)

assert len(w) == 1
assert issubclass(w[0].category, UserWarning)
assert "warning" in str(w[0].message)

# 使用 pytest.warns
def test_pytest_warns():
with pytest.warns(UserWarning, match="warning"):
warnings.warn("This is a warning", UserWarning)

def test_no_warning():
with pytest.warns(None) as warning_list:
# 不应该产生任何警告
pass

Fixture

定义Fixture

Fixture 是 Pytest 的依赖注入机制,用于提供测试所需的测试数据、对象或资源:

import pytest

# 简单 fixture
@pytest.fixture
def sample_data():
return [1, 2, 3, 4, 5]

def test_sum(sample_data):
assert sum(sample_data) == 15

# yield fixture(用于资源清理)
@pytest.fixture
def database():
# setup
db = {"users": []}
print("\nSetup database")

yield db

# teardown
print("\nTeardown database")
db.clear()

def test_database(database):
database["users"].append("Alice")
assert len(database["users"]) == 1

# 带参数的 fixture
@pytest.fixture
def user():
class User:
def __init__(self, name, email):
self.name = name
self.email = email

return User("Bob", "bob@example.com")

def test_user_email(user):
assert "@" in user.email
assert user.name == "Bob"

作用域

Fixture 支持不同的作用域,控制其生命周期:

import pytest

# function 作用域(默认):每个测试函数都重新创建
@pytest.fixture(scope="function")
def function_resource():
print("\nFunction scope setup")
yield 1
print("\nFunction scope teardown")

# class 作用域:每个测试类创建一次
@pytest.fixture(scope="class")
def class_resource():
print("\nClass scope setup")
yield 2
print("\nClass scope teardown")

# module 作用域:每个模块创建一次
@pytest.fixture(scope="module")
def module_resource():
print("\nModule scope setup")
yield 3
print("\nModule scope teardown")

# session 作用域:整个测试会话创建一次
@pytest.fixture(scope="session")
def session_resource():
print("\nSession scope setup")
yield 4
print("\nSession scope teardown")

class TestFixtureScopes:
def test_one(self, function_resource, class_resource, module_resource):
assert function_resource == 1

def test_two(self, function_resource, class_resource, module_resource):
assert function_resource == 1

参数化

对 fixture 进行参数化:

import pytest

@pytest.fixture(params=[1, 2, 3])
def number(request):
return request.param

def test_numbers(number):
# 这个测试会运行3次,分别使用 1, 2, 3
assert number > 0

# 多参数组合
@pytest.fixture(params=["alice", "bob", "charlie"])
def username(request):
return request.param

@pytest.fixture(params=["reader", "writer", "admin"])
def role(request):
return request.param

def test_user_roles(username, role):
# 会运行 3x3=9 次,所有组合
assert isinstance(username, str)
assert isinstance(role, str)

conftest.py

conftest.py 用于共享 fixture 和配置:

# conftest.py

import pytest
import tempfile
import os

# 全局可用的 fixture
@pytest.fixture(scope="session")
def global_config():
return {
"database_url": "sqlite:///:memory:",
"debug": True
}

# 自动使用的 fixture
@pytest.fixture(autouse=True)
def setup_test_environment():
"""每个测试自动使用"""
print("\n=== Test started ===")
yield
print("\n=== Test finished ===")

# 临时目录 fixture
@pytest.fixture(scope="function")
def temp_dir():
"""创建临时目录"""
with tempfile.TemporaryDirectory() as tmpdir:
old_cwd = os.getcwd()
os.chdir(tmpdir)
yield tmpdir
os.chdir(old_cwd)

# 测试数据 fixture
@pytest.fixture
def sample_users():
return [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "user"},
]

在其他测试文件中使用:

# test_user_service.py

def test_user_count(sample_users):
assert len(sample_users) == 3

def test_admin_exists(sample_users):
admins = [u for u in sample_users if u["role"] == "admin"]
assert len(admins) == 1

参数化

单个参数

使用 @pytest.mark.parametrize 进行参数化:

import pytest

@pytest.mark.parametrize("input,expected", [
(1, 2),
(2, 4),
(3, 6),
(4, 8),
])
def test_multiply_by_two(input, expected):
assert input * 2 == expected

# 单个参数列表
@pytest.mark.parametrize("number", [1, 2, 3, 4, 5])
def test_is_positive(number):
assert number > 0

# 字符串参数化
@pytest.mark.parametrize("name", ["Alice", "Bob", "Charlie"])
def test_greeting(name):
assert f"Hello, {name}" == f"Hello, {name}"

多个参数

@pytest.mark.parametrize("x,y,expected", [
(1, 2, 3),
(2, 3, 5),
(10, -5, 5),
(0, 0, 0),
])
def test_addition(x, y, expected):
assert x + y == expected

# 参数化测试类
@pytest.mark.parametrize("a,b", [
(1, 2),
(3, 4),
])
class TestMath:
def test_add(self, a, b):
assert a + b > 0

def test_multiply(self, a, b):
assert a * b >= 0

参数组合

使用多个参数化标记产生笛卡尔积:

@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_combinations(x, y):
# 会运行 4 次:(1,10), (1,20), (2,10), (2,20)
assert x + y > 0

# 条件参数化
@pytest.mark.parametrize("x", [1, 2, 3, None])
def test_not_none(x):
if x is None:
pytest.skip("Skip None values")
assert x is not None

# 参数 IDs
@pytest.mark.parametrize("input,expected", [
(2, True),
(3, True),
(4, False),
(5, False),
], ids=["prime_2", "prime_3", "composite_4", "composite_5"])
def test_is_prime(input, expected):
"""测试会在报告中显示自定义 ID"""
assert is_prime(input) == expected

def is_prime(n):
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True

测试组织

测试类

使用测试类组织相关测试:

class TestUserService:
"""用户服务测试类"""

@pytest.fixture(autouse=True)
def setup(self):
"""每个测试方法前执行"""
self.service = UserService()
yield
self.service = None

def test_create_user(self):
user = self.service.create_user("Alice", "alice@example.com")
assert user.name == "Alice"

def test_delete_user(self):
user = self.service.create_user("Bob", "bob@example.com")
result = self.service.delete_user(user.id)
assert result is True

def test_find_nonexistent_user(self):
with pytest.raises(UserNotFoundError):
self.service.find_user(999)

class TestAuthService:
"""认证服务测试类"""

def test_login_success(self):
assert True

def test_login_failure(self):
assert True

测试包

按目录结构组织测试:

tests/
├── conftest.py # 共享 fixtures
├── unit/
│ ├── conftest.py # 单元测试 fixtures
│ ├── test_user.py
│ └── test_auth.py
├── integration/
│ ├── conftest.py # 集成测试 fixtures
│ └── test_api.py
└── e2e/
├── conftest.py # 端到端测试 fixtures
└── test_workflow.py
# tests/conftest.py
@pytest.fixture(scope="session")
def test_database():
"""全局测试数据库"""
db = create_test_database()
yield db
db.close()

# tests/unit/conftest.py
@pytest.fixture
def mock_db():
"""单元测试使用 mock 数据库"""
return MockDatabase()

# tests/integration/conftest.py
@pytest.fixture
def real_db(test_database):
"""集成测试使用真实测试数据库"""
return test_database

标记测试

使用标记对测试进行分类:

import pytest

# 定义标记
pytestmark = [
pytest.mark.version("2.0"),
]

# 单个标记
@pytest.mark.unit
def test_simple_function():
assert 1 + 1 == 2

# 多个标记
@pytest.mark.unit
@pytest.mark.math
def test_calculate():
assert 2 * 3 == 6

# 自定义标记
@pytest.mark.slow
def test_long_running():
import time
time.sleep(5)
assert True

@pytest.mark.integration
def test_database_integration():
assert True

@pytest.markSmoke
def test_smoke():
assert True

# 跳过标记
@pytest.mark.skip(reason="Feature not implemented yet")
def test_future_feature():
assert True

# 在类上使用标记
@pytest.mark.web
class TestWebAPI:
def test_get(self):
assert True

def test_post(self):
assert True

pytest.ini 中配置标记:

[pytest]
markers =
unit: Unit tests
integration: Integration tests
slow: Slow running tests
web: Web API tests
Smoke: Smoke tests

运行特定标记的测试:

# 只运行 unit 测试
pytest -m unit

# 运行多个标记
pytest -m "unit or integration"

# 排除标记
pytest -m "not slow"

# 标记组合
pytest -m "unit and not slow"

跳过与失败

skip

无条件跳过测试:

import pytest

@pytest.mark.skip(reason="暂时跳过此测试")
def test_something():
assert True

def test_another():
pytest.skip("跳过这个测试")

# 条件跳过
def test_conditional_skip():
if not import_module("optional_feature"):
pytest.skip("需要 optional_feature 模块")
assert True

# 跳过整个类
@pytest.mark.skip(reason="等待修复")
class TestBrokenFeature:
def test_one(self):
assert True

def test_two(self):
assert True

skipif

条件性跳过:

import sys
import pytest

@pytest.mark.skipif(sys.version_info < (3, 8),
reason="需要 Python 3.8+")
def test_new_feature():
assert True

@pytest.mark.skipif(sys.platform == "win32",
reason="Unix 系统测试")
def test_unix_only():
assert True

# 多个条件
@pytest.mark.skipif(
(sys.version_info < (3, 8)) or (sys.platform == "win32"),
reason="需要 Python 3.8+ 且非 Windows"
)
def test_combination():
assert True

# 使用 fixture 条件
@pytest.fixture
def skip_if_no_db():
if not database_available():
pytest.skip("数据库不可用")

def test_database(skip_if_no_db):
assert True

xfail

预期失败的测试:

@pytest.mark.xfail
def test_expected_failure():
"""预期这个测试会失败"""
assert 1 + 1 == 3

@pytest.mark.xfail(reason="Bug #123 已知问题")
def test_known_bug():
"""已知 Bug,暂时不修复"""
assert False

# 条件性 xfail
@pytest.mark.xfail(sys.version_info < (3, 9),
reason="Python 3.9 之前有 bug")
def test_version_specific():
assert True

# 严格模式(必须失败,否则算失败)
@pytest.mark.xfail(strict=True, reason="必须失败")
def test_must_fail():
assert True # 这会被标记为失败

# 在测试中动态 xfail
def test_runtime_xfail():
if some_condition():
pytest.xfail("条件不满足,预期失败")
assert True

# xfail 但不运行
@pytest.mark.xfail(run=False)
def test_not_run():
assert False

Mock与Patch

unittest.mock

使用标准库的 mock 功能:

from unittest.mock import Mock, MagicMock, patch

def test_mock_basic():
"""基础 Mock 使用"""
mock = Mock()
mock.method()
mock.method.assert_called_once()

def test_mock_return_value():
"""设置返回值"""
mock = Mock()
mock.method.return_value = 42

result = mock.method()
assert result == 42
mock.method.assert_called_once()

def test_mock_side_effect():
"""副作用:每次调用返回不同值"""
mock = Mock()
mock.method.side_effect = [1, 2, 3]

assert mock.method() == 1
assert mock.method() == 2
assert mock.method() == 3

def test_mock_exception():
"""抛出异常"""
mock = Mock()
mock.method.side_effect = ValueError("Error message")

with pytest.raises(ValueError, match="Error message"):
mock.method()

def test_mock_attributes():
"""Mock 属性"""
mock = Mock()
mock.name = "Alice"
mock.age = 30

assert mock.name == "Alice"
assert mock.age == 30

mocker

pytest-mock 插件提供的 mocker fixture:

import pytest

def test_mocker_basic(mocker):
"""使用 mocker fixture"""
mock_func = mocker.Mock()
mock_func.return_value = 42

assert mock_func() == 42
mock_func.assert_called_once()

def test_mocker_patch(mocker):
"""Patch 对象"""
mocker.patch("module.function", return_value="mocked")

from module import function
assert function() == "mocked"

def test_mocker_patch_class(mocker):
"""Patch 类"""
MockClass = mocker.patch("module.MyClass")
instance = MockClass.return_value
instance.method.return_value = "result"

from module import MyClass
obj = MyClass()
assert obj.method() == "result"

def test_mocker_spy(mocker):
"""Spy 函数:保留原函数但监控调用"""
def original_func(x):
return x * 2

spy = mocker.spy(original_func)
result = original_func(5)

assert result == 10
spy.assert_called_once_with(5)

Patch

使用 patch 装饰器或上下文管理器:

from unittest.mock import patch

# 装饰器方式
@patch("module.external_api_call")
def test_with_decorator(mock_api):
mock_api.return_value = {"status": "ok"}

result = call_external_api()
assert result == {"status": "ok"}
mock_api.assert_called_once()

# 多个 patch
@patch("module.database")
@patch("module.cache")
def test_multiple_patches(mock_cache, mock_db):
mock_db.query.return_value = [1, 2, 3]
mock_cache.get.return_value = None

result = get_data()
assert len(result) == 3

# 上下文管理器方式
def test_with_context_manager():
with patch("module.time consuming_operation") as mock_op:
mock_op.return_value = "fast result"

result = time_consuming_operation()
assert result == "fast result"

# Patch 对象方法
class TestService:
@patch("object.requests.get")
def test_api_call(self, mock_get):
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": "value"}
mock_get.return_value = mock_response

service = TestService()
data = service.fetch_data()

assert data == {"data": "value"}
mock_get.assert_called_once()

测试覆盖率

pytest-cov

安装和配置:

pip install pytest-cov
# 生成覆盖率报告
pytest --cov=my_module tests/

# 生成终端报告
pytest --cov=my_module --cov-report=term

# 生成 HTML 报告
pytest --cov=my_module --cov-report=html

# 生成 XML 报告(用于 CI)
pytest --cov=my_module --cov-report=xml

# 组合多种报告
pytest --cov=my_module --cov-report=term-missing --cov-report=html

# 设置覆盖率阈值(低于阈值失败)
pytest --cov=my_module --cov-fail-under=80

覆盖率报告

# pytest.ini 或 setup.cfg 配置

[tool:pytest]
addopts = --cov=src --cov-report=term-missing --cov-report=html --cov-fail-under=80

[coverage:run]
source = src
omit =
*/tests/*
*/test_*.py
*/__init__.py
*/migrations/*

[coverage:report]
precision = 2
show_missing = True
skip_covered = False

[coverage:html]
directory = htmlcov

分支覆盖率:

# 启用分支覆盖率
pytest --cov=my_module --cov-branch

按模块查看覆盖率:

# .coveragerc

[coverage:run]
branch = True
source =
my_app
my_utils

[coverage:report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:

并行测试

pytest-xdist

安装和基础使用:

pip install pytest-xdist
# 使用所有 CPU 核心
pytest -n auto

# 指定进程数
pytest -n 4

# 分布式测试(多台机器)
pytest -dist --tx 3*popen//id=worker1 --tx 3*popen//id=worker2
# 确保 fixture 线程安全

@pytest.fixture(scope="session")
def session_data():
"""session 作用域在并行测试时只创建一次"""
return {"counter": 0}

@pytest.fixture
def isolated_data():
"""每个测试独立的数据"""
return {"value": 42}

插件生态

常用插件

# 异步测试
pip install pytest-asyncio

# Django 测试
pip install pytest-django

# Flask 测试
pip install pytest-flask

# 临时目录和文件
pip install pytest-tmpdir

# 测试时间排序
pip install pytest-testmon

# 随机化测试顺序
pip install pytest-randomly

# 性能测试
pip install pytest-benchmark

# HTML 报告
pip install pytest-html

# 重试失败的测试
pip install pytest-rerunfailures

自定义插件

创建自定义 pytest 插件:

# my_pytest_plugin.py

import pytest

def pytest_configure(config):
"""pytest 配置时调用"""
config.addinivalue_line(
"markers", "env(name): mark test to run only on named environment"
)

def pytest_collection_modifyitems(config, items):
"""修改收集到的测试项"""
for item in items:
# 自动添加标记
if "slow" in item.nodeid:
item.add_marker(pytest.mark.slow)

@pytest.fixture
def custom_fixture():
"""自定义 fixture"""
return "custom value"

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""增强测试报告"""
outcome = yield
report = outcome.get_result()

# 添加自定义信息
if report.when == "call":
report.custom_info = "Additional test info"

def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""添加终端摘要信息"""
terminalreporter.write_sep("=", "Custom Summary Line")

注册插件:

# setup.py 或 pyproject.toml

setup(
name="my-pytest-plugin",
entry_points={
"pytest11": [
"my_plugin = my_pytest_plugin",
],
},
)

或在 conftest.py 中启用:

# conftest.py

pytest_plugins = ["my_pytest_plugin"]

测试配置

pytest.ini

配置文件示例:

# pytest.ini

[pytest]
# 测试发现路径
testpaths = tests

# 测试文件模式
python_files = test_*.py *_test.py

# 测试类模式
python_classes = Test*

# 测试函数模式
python_functions = test_*

# 默认命令行选项
addopts =
-v
--strict-markers
--disable-warnings
--cov=src
--cov-report=term-missing
--cov-report=html

# 最小版本
minversion = 7.0

# 标记定义
markers =
unit: 单元测试
integration: 集成测试
slow: 慢速测试
web: Web API 测试
Smoke: 冒烟测试

# 日志配置
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S

# 警告设置
filterwarnings =
error
ignore::UserWarning
ignore::DeprecationWarning

命令行选项

常用命令行选项:

# 显示详细输出
pytest -v

# 显示极简输出
pytest -q

# 显示打印输出
pytest -s

# 显示本地变量
pytest -l

# 只运行失败的测试
pytest --lf

# 先运行失败的测试
pytest --ff

# 停止在第 N 个失败
pytest -x # 第一个失败后停止
pytest --maxfail=3 # 3 个失败后停止

# 捕获输出
pytest --capture=no # 不捕获输出
pytest --capture=sys # 捕获到 sys.stdout/stderr

# 显示最慢的 10 个测试
pytest --durations=10

# 显示 fixture 用法
pytest --fixtures

# 显示标记
pytest --markers

# PDB 调试
pytest --pdb # 失败时进入 PDB
pytest --trace # 每个测试前进入 PDB

# 随机化测试顺序
pytest --random-order

# 重复运行测试
pytest --count=5

# 并行运行
pytest -n 4

最佳实践

1. 测试命名和组织

# ✅ 好的命名
def test_user_can_login_with_valid_credentials():
pass

def test_user_cannot_login_with_invalid_password():
pass

# ❌ 不好的命名
def test_login():
pass

def test_login2():
pass

2. Fixture 设计

# ✅ 好的设计:职责单一
@pytest.fixture
def clean_database():
"""干净的数据库"""
db = create_database()
db.clear()
yield db
db.close()

@pytest.fixture
def sample_user():
"""测试用户"""
return User(name="Alice", email="alice@example.com")

# ❌ 不好的设计:职责混乱
@pytest.fixture
def messy_fixture():
db = create_database()
user = User("Alice")
client = APIClient()
yield db, user, client
# 清理逻辑混乱

3. 避免测试依赖

# ✅ 好的做法:测试独立
class TestUserCreation:
def test_create_user_with_valid_data(self):
user = create_user("Alice", "alice@example.com")
assert user.name == "Alice"

def test_create_user_with_duplicate_email(self):
with pytest.raises(DuplicateEmailError):
create_user("Alice", "alice@example.com")
create_user("Bob", "alice@example.com")

# ❌ 不好的做法:测试依赖顺序
class TestUserWorkflow:
def test_1_create_user(self):
self.user = create_user("Alice")

def test_2_update_user(self):
# 依赖 test_1_create_user 先执行
self.user.name = "Bob"

4. 使用标记组织测试

# 快速单元测试
@pytest.mark.unit
@pytest.mark.fast
def test_simple_calculation():
assert 1 + 1 == 2

# 慢速集成测试
@pytest.mark.integration
@pytest.mark.slow
def test_database_integration():
# 耗时操作
pass

5. Mock 的正确使用

# ✅ Mock 外部依赖
@patch("requests.get")
def test_api_call(mock_get):
mock_get.return_value.status_code = 200
response = call_external_api()
assert response.status_code == 200

# ❌ 不要 Mock 被测试的对象
@patch("module.my_function")
def test_my_function(mock_func):
# 错误:mock 了要测试的函数
result = my_function()
assert result == "mocked"

6. 参数化减少重复

# ✅ 使用参数化
@pytest.mark.parametrize("input,expected", [
("", True),
(" ", True),
("\t\n", True),
("text", False),
])
def test_is_blank(input, expected):
assert is_blank(input) == expected

# ❌ 重复的测试
def test_is_blank_empty_string():
assert is_blank("") == True

def test_is_blank_whitespace():
assert is_blank(" ") == True

def test_is_blank_text():
assert is_blank("text") == False

7. 清晰的断言消息

# ✅ 自定义断言消息
def test_user_age():
user = User(age=15)
assert user.is_adult() is False, "15 岁用户应该是未成年人"

# ❌ 不清晰的断言
def test_user_age():
user = User(age=15)
assert user.is_adult() is False

8. 测试数据管理

# ✅ 使用 fixtures 管理测试数据
@pytest.fixture
def user_data():
return {
"valid": {
"name": "Alice",
"email": "alice@example.com",
"age": 25,
},
"invalid": {
"name": "",
"email": "invalid-email",
"age": -1,
}
}

def test_create_user_with_valid_data(user_data):
user = User(**user_data["valid"])
assert user.is_valid()

# ❌ 硬编码测试数据
def test_create_user_with_valid_data():
user = User(name="Alice", email="alice@example.com", age=25)
assert user.is_valid()

CI/CD集成

GitHub Actions

# .github/workflows/test.yml

name: Tests

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, "3.10", "3.11"]

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-xdist
pip install -r requirements.txt

- name: Run tests
run: |
pytest --cov=src --cov-report=xml --cov-report=term

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella

GitLab CI

# .gitlab-ci.yml

test:
image: python:3.10
parallel:
matrix:
- PYTHON_VERSION: ["3.8", "3.9", "3.10", "3.11"]

before_script:
- python -m pip install --upgrade pip
- pip install pytest pytest-cov
- pip install -r requirements.txt

script:
- pytest --cov=src --cov-report=xml --cov-report=html

coverage: '/(?i)lines.*: \d+\.\d+%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml

Jenkins

// Jenkinsfile

pipeline {
agent any

stages {
stage('Setup') {
steps {
sh 'python -m pip install --upgrade pip'
sh 'pip install pytest pytest-cov'
sh 'pip install -r requirements.txt'
}
}

stage('Test') {
steps {
sh 'pytest --cov=src --cov-report=xml --cov-report=term --junitxml=pytest-report.xml'
}
}

stage('Report') {
steps {
junit 'pytest-report.xml'
publishCoverage adapters: [coberturaAdapter('coverage.xml')]
}
}
}

post {
always {
archiveArtifacts artifacts: 'htmlcov/**/*'
}
}
}

Docker Compose

# docker-compose.yml

version: '3.8'

services:
app:
build: .
volumes:
- .:/app
command: pytest --cov=src

test:
build: .
volumes:
- .:/app
command: >
sh -c "
pytest --cov=src --cov-report=xml --cov-report=term &&
pytest --html=report.html --self-contained-html
"

预提交钩子:

# .pre-commit-config.yaml

repos:
- repo: https://github.com/pytest-dev/pytest
rev: 7.1.2
hooks:
- id: pytest
args: [--cov=src, --cov-fail-under=80]

通过 Pytest,你可以构建完整、可靠、易维护的测试套件,确保代码质量和系统稳定性。