引言

在软件开发中,测试是保证代码质量的关键环节。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 夹具的工作原理

  1. 发现参数:pytest 发现测试函数有参数 language_survey
  2. 查找夹具:根据参数名找到同名的 @pytest.fixture 装饰的函数
  3. 执行夹具:执行 language_survey() 函数,获取返回值
  4. 注入参数:将返回值赋值给测试函数的参数
  5. 执行测试:测试函数使用已创建好的实例对象

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


更多推荐