从零开始学习 pytest:Python 测试框架完全指南
·
引言
在软件开发中,测试是保证代码质量的关键环节。pytest 是 Python 最流行的测试框架之一,它简单易用、功能强大,支持丰富的插件生态系统。无论你是测试新手还是有经验的开发者,掌握 pytest 都能显著提升你的测试效率和代码质量。
本文将带你从零开始,全面学习 pytest 的核心概念、安装配置、基本用法、高级特性以及最佳实践。
1. 安装与配置
1.1 创建测试环境
# 创建虚拟环境
python3 -m venv venv
# 激活虚拟环境
source venv/bin/activate
# 在虚拟环境中安装 pytest
pip install pytest
2. 第一个 pytest 测试
2.1 编写待测试函数
创建 functions.py 文件:
# functions.py
def get_full_name(first, last, middle=""):
"""返回格式化的全名"""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
2.2 编写测试文件
创建 test_functions.py 文件:
# test_functions.py
from functions import get_full_name
def test_get_full_name():
"""测试不带中间名的情况"""
assert get_full_name("wang", "haodong") == "Wang Haodong"
def test_get_full_name_with_middle():
"""测试带中间名的情况"""
assert get_full_name("wang", "hao", "dong") == "Wang Dong Hao"
2.3 运行测试
pytest test_functions.py
你会看到类似这样的输出:
========================= test session starts =========================
platform linux -- Python 3.9.0, pytest-7.0.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 2 items
test_functions.py .. [100%]
========================== 2 passed in 0.01s ==========================
3. pytest 测试文件命名规则
pytest 会自动发现并运行测试文件,遵循以下命名规则:
-
测试文件:以
test_开头或以_test.py结尾- ✅
test_functions.py - ✅
functions_test.py - ❌
my_tests.py(不会被自动发现)
- ✅
-
测试函数:以
test_开头- ✅
def test_addition(): - ❌
def check_addition():
- ✅
-
测试类:以
Test开头(且不能有__init__方法)- ✅
class TestLogin: - ❌
class LoginTest:
- ✅
4. 断言语句详解
pytest 使用 Python 原生的 assert 语句进行断言,简洁直观:
| 断言语句 | 说明 | 示例 |
|---|---|---|
assert a == b |
断言两个值相等 | assert 1 + 1 == 2 |
assert a != b |
断言两个值不相等 | assert 1 != 2 |
assert a |
断言 a 的布尔值为 True | assert True |
assert not a |
断言 a 的布尔值为 False | assert not False |
assert element in list |
断言元素在列表中 | assert 1 in [1, 2, 3] |
assert element not in list |
断言元素不在列表中 | assert 4 not in [1, 2, 3] |
assert a > b |
断言 a 大于 b | assert 5 > 3 |
assert a < b |
断言 a 小于 b | assert 2 < 10 |
assert a is None |
断言 a 是 None | assert result is None |
assert a is not None |
断言 a 不是 None | assert result is not None |
4.1 更复杂的示例
def get_location(city, country, population=None):
"""返回城市位置信息"""
if population:
return f"{city}, {country} - Population: {population}"
return f"{city}, {country}"
# 测试代码
from city_functions import get_location
def test_get_location():
"""测试不带人口信息的情况"""
assert get_location("Beijing", "China") == "Beijing, China"
def test_get_location_with_population():
"""测试带人口信息的情况"""
result = get_location("New York", "USA", 8000000)
assert result == "New York, USA - Population: 8000000"
assert "Population" in result
assert "8000000" in result
5. 测试类(Class Testing)
5.1 业务类代码
创建 survey.py:
# survey.py
class AnonymousSurvey:
"""匿名调查类"""
def __init__(self, question):
"""初始化调查问题"""
self.question = question
self.responses = []
def show_question(self):
"""显示问题"""
print(self.question)
def store_response(self, new_response):
"""保存回答"""
self.responses.append(new_response)
def show_results(self):
"""显示所有回答"""
print("Survey results:")
for response in self.responses:
print(f"- {response}")
5.2 测试类代码
创建 test_survey.py:
# test_survey.py
import pytest
from survey import AnonymousSurvey
def test_store_single_response():
"""测试保存单个回答"""
question = "你学的第一个外语是什么?"
language_survey = AnonymousSurvey(question)
language_survey.store_response("英语")
# 验证回答已保存
assert "英语" in language_survey.responses
assert len(language_survey.responses) == 1
def test_store_three_responses():
"""测试保存多个回答"""
question = "你学的第一个外语是什么?"
language_survey = AnonymousSurvey(question)
responses = ["英语", "法语", "日语"]
for response in responses:
language_survey.store_response(response)
# 验证所有回答都已保存
assert len(language_survey.responses) == 3
for item in responses:
assert item in language_survey.responses
6. 夹具(Fixture)的使用
6.1 为什么需要夹具?
当多个测试函数都需要相同的测试数据或对象时,代码会出现重复:
# 重复的初始化代码
def test_one():
survey = AnonymousSurvey("问题1")
# ... 测试逻辑
def test_two():
survey = AnonymousSurvey("问题1") # 重复!
# ... 测试逻辑
6.2 使用夹具解决代码重复
# test_survey_with_fixture.py
import pytest
from survey import AnonymousSurvey
# 定义夹具
@pytest.fixture
def language_survey():
"""返回一个配置好的调查对象"""
question = "你学的第一个外语是什么?"
return AnonymousSurvey(question)
# 使用夹具
def test_store_single_response(language_survey):
"""测试保存单个回答(使用夹具)"""
language_survey.store_response("英语")
assert "英语" in language_survey.responses
assert len(language_survey.responses) == 1
def test_store_three_responses(language_survey):
"""测试保存多个回答(使用夹具)"""
responses = ["英语", "法语", "日语"]
for response in responses:
language_survey.store_response(response)
assert len(language_survey.responses) == 3
for item in responses:
assert item in language_survey.responses
def test_empty_survey(language_survey):
"""测试初始状态(使用夹具)"""
assert len(language_survey.responses) == 0
assert language_survey.question == "你学的第一个外语是什么?"
6.3 夹具的工作原理
- 发现参数:pytest 发现测试函数有参数
language_survey - 查找夹具:根据参数名找到同名的
@pytest.fixture装饰的函数 - 执行夹具:执行
language_survey()函数,获取返回值 - 注入参数:将返回值赋值给测试函数的参数
- 执行测试:测试函数使用已创建好的实例对象
6.4 夹具的高级用法
import pytest
# 夹具可以返回任何类型的数据
@pytest.fixture
def sample_data():
"""返回测试数据"""
return {
"name": "张三",
"age": 25,
"skills": ["Python", "pytest", "Django"]
}
# 夹具可以依赖其他夹具
@pytest.fixture
def user_with_data(sample_data):
"""使用 sample_data 夹具创建用户对象"""
class User:
def __init__(self, data):
self.name = data["name"]
self.age = data["age"]
self.skills = data["skills"]
return User(sample_data)
def test_user_creation(user_with_data):
"""测试用户创建"""
assert user_with_data.name == "张三"
assert user_with_data.age == 25
assert "Python" in user_with_data.skills
7 基本运行命令
# 运行所有测试
pytest
# 运行指定测试文件
pytest test_functions.py
更多推荐
所有评论(0)