1. 项目概述:为什么选择pytest作为你的自动化起点?

如果你刚开始接触Python自动化测试,或者正在从unittest、nose等框架转向更现代的工具,那么“pytest的基础使用”这个标题背后,其实隐藏着一个非常核心的命题:如何选择一个既强大又优雅的测试框架,来支撑你从零到一的自动化旅程?我见过太多团队和个人,在项目初期因为框架选型不当,导致后期维护成本指数级增长,测试代码变得臃肿不堪。pytest之所以能成为Python测试领域的“事实标准”,绝非偶然。

简单来说,pytest是一个让编写和运行测试变得极其简单、功能却异常强大的全功能测试框架。它的设计哲学是“约定优于配置”和“即插即用”。你不需要继承某个特定的类,不需要记住一堆复杂的断言方法名(直接用Python自带的 assert 就行),它还能自动发现并运行你的测试用例。对于新手,它的学习曲线平缓;对于老手,其丰富的插件生态(超过1000个插件)又能满足各种复杂场景,从简单的单元测试到复杂的UI、接口自动化,甚至性能测试都能驾驭。这次,我们就从最基础的部分开始,手把手带你搭建一个稳固的pytest自动化测试地基,让你后续无论是做接口、Web UI还是App自动化,都能事半功倍。

2. 环境搭建与项目初始化:打造专属的测试沙盒

在开始写第一行测试代码之前,建立一个干净、隔离的Python环境是专业开发的第一步。这能避免不同项目间的依赖冲突,也是团队协作和持续集成的基础。

2.1 Python与虚拟环境准备

首先,确保你的系统安装了Python 3.7或更高版本。在命令行输入 python --version python3 --version 确认。我强烈建议使用Python 3.8以上版本,以获得更好的性能和语言特性支持。

接下来,为我们的pytest自动化项目创建一个独立的虚拟环境。虚拟环境就像是一个独立的“沙盒”,在这个沙盒里安装的所有包都不会影响到系统全局的Python环境。

# 1. 创建项目目录并进入
mkdir pytest-automation-starter
cd pytest-automation-starter

# 2. 创建虚拟环境(以环境目录名为`.venv`为例)
python -m venv .venv

# 3. 激活虚拟环境
# 在Windows上(CMD或PowerShell):
# .venv\Scripts\activate
# 在macOS/Linux上:
source .venv/bin/activate

激活后,你的命令行提示符前通常会显示 (.venv) ,表示你已经在这个虚拟环境中了。所有后续的 pip install 操作都只会影响这个环境。

实操心得 :我习惯将虚拟环境目录命名为 .venv ,并在 .gitignore 文件中加入 .venv/ ,这样它就不会被提交到版本库中。每个开发者克隆项目后,都需要自己创建并激活虚拟环境,然后通过 requirements.txt 安装依赖,这保证了环境的一致性。

2.2 核心依赖安装与固化

在激活的虚拟环境中,我们安装pytest和常用的HTTP请求库 requests (为后续接口自动化示例做准备)。

# 安装pytest和requests
pip install pytest requests

# 可选但推荐:安装用于生成更美观HTML报告的插件
pip install pytest-html

# 将当前环境的依赖列表导出到requirements.txt文件
pip freeze > requirements.txt

现在,你的项目根目录下会生成一个 requirements.txt 文件,里面记录了精确的包版本号,例如 pytest==8.0.0 。这个文件是项目环境的“蓝图”,至关重要。

注意事项 pip freeze 会导出当前环境下 所有 已安装的包,包括你间接依赖的。对于更纯净的控制,你可以手动维护一个 requirements.in 文件,只列出直接依赖(如 pytest , requests ),然后使用 pip-compile (来自 pip-tools 包)来生成锁定版本的 requirements.txt 。但对于初学者和小型项目,直接 pip freeze 更简单直接。

3. pytest核心概念与第一个测试用例

环境就绪,让我们立刻动手写第一个测试,在实践中理解pytest的核心魅力。

3.1 测试发现规则与编写规范

pytest的智能之处在于它的“测试发现”机制。它会自动搜索当前目录及其子目录下,符合以下命名规则的文件和函数:

  • 测试文件:以 test_ 开头或以 _test.py 结尾的文件(例如 test_demo.py demo_test.py )。
  • 测试函数:在测试文件中,以 test_ 开头的函数。
  • 测试类:以 Test 开头的类(且不能有 __init__ 方法),类内部以 test_ 开头的方法。

我们在项目根目录下创建一个 tests 文件夹,并在其中创建第一个测试文件 test_basic.py

# tests/test_basic.py

def test_addition():
    """一个最简单的测试函数,验证加法"""
    assert 1 + 1 == 2

def test_string_concatenation():
    """测试字符串拼接"""
    result = "Hello, " + "pytest!"
    assert result == "Hello, pytest!"
    # 注意:pytest直接使用Python的assert语句,失败时会给出详细的差异信息

class TestCalculator:
    """一个测试类,用于组织相关的测试方法"""
    
    def test_subtraction(self):
        """测试减法"""
        assert 5 - 3 == 2
    
    def test_list_contains(self):
        """测试成员断言"""
        items = ['pytest', 'unittest', 'nose']
        assert 'pytest' in items
        assert 'robot' not in items  # 断言不在列表中

3.2 运行测试与解读输出

保存文件后,在项目根目录(确保虚拟环境已激活)下运行最简单的命令:

pytest

你会看到类似下面的输出:

============================= test session starts ==============================
platform darwin -- Python 3.11.6, pytest-8.0.0, pluggy-1.4.0
rootdir: /path/to/pytest-automation-starter
collected 4 items

tests/test_basic.py ....                                                 [100%]

============================== 4 passed in 0.02s ===============================

这表示pytest自动发现了4个测试(一个点 . 代表一个通过的测试),并且全部通过。这就是“约定优于配置”的力量——你不需要写任何运行脚本。

常用运行参数解析:

  • pytest -v : 以详细模式运行,会显示每个测试用例的名称和结果。
  • pytest tests/test_basic.py::test_addition : 运行指定文件的指定测试函数。
  • pytest tests/test_basic.py::TestCalculator::test_subtraction : 运行指定测试类中的指定方法。
  • pytest -k “addition or string” : 使用关键字表达式筛选包含“addition”或“string”的测试用例来运行。
  • pytest -x : 遇到第一个失败用例时立即停止测试。
  • pytest --maxfail=2 : 当失败用例达到2个时停止测试。

实操心得 :在开发调试阶段,我频繁使用 pytest -v -k “keyword” 来快速运行我当前正在修改的那部分测试,效率极高。 -x 参数在排查复杂测试集的第一个失败点时也非常有用。

4. 深入fixture:pytest的“瑞士军刀”

如果说 assert 是pytest的“心脏”,那么 fixture 就是它的“灵魂”。它是pytest用于实现测试前置条件(Setup)、后置清理(Teardown)和数据共享的核心机制,功能远超传统的 setUp / tearDown 方法。

4.1 fixture的基本定义与使用

fixture通过 @pytest.fixture 装饰器定义。它最大的特点是 依赖注入 ,你只需要在测试函数参数中声明需要的fixture名字,pytest会自动在运行测试前调用它,并将返回值注入。

# tests/test_fixture_basic.py
import pytest

@pytest.fixture
def sample_data():
    """一个简单的fixture,返回一个列表"""
    print("\n(Setup)准备测试数据...")
    data = [1, 2, 3, 4, 5]
    yield data  # yield之前的代码是setup,yield之后的是teardown
    print("(Teardown)清理测试数据...")

def test_data_length(sample_data):  # 通过参数请求fixture
    """测试fixture提供的数据长度"""
    assert len(sample_data) == 5
    assert sample_data[0] == 1

def test_data_sum(sample_data):
    """测试fixture提供的数据求和"""
    assert sum(sample_data) == 15

运行测试,你会看到每次测试函数执行前后,fixture中的 print 语句都会执行。 yield 是fixture实现teardown的关键, yield 之前的代码在测试前执行(setup), yield 返回的值注入给测试函数,测试函数执行完毕后,会回到fixture中执行 yield 之后的代码(teardown)。

4.2 fixture的作用域(scope)

fixture默认的作用域是 function ,即每个测试函数都会执行一次。但在很多场景下,这是低效的。pytest提供了多种作用域:

  • function (默认): 每个测试函数运行一次。
  • class : 每个测试类运行一次。
  • module : 每个模块(文件)运行一次。
  • package : 每个包运行一次。
  • session : 一次测试会话(即一次 pytest 命令)只运行一次。
import pytest
import time

@pytest.fixture(scope="module")
def expensive_resource():
    """模拟一个初始化耗时较长的资源,如数据库连接"""
    print("\n>>> 初始化昂贵的模块级资源(如数据库连接)")
    start = time.time()
    # 模拟耗时操作
    time.sleep(1)
    resource = {"db_conn": "connected", "timestamp": start}
    yield resource
    print(">>> 关闭模块级资源连接")
    resource["db_conn"] = "disconnected"

def test_db_query_1(expensive_resource):
    assert expensive_resource["db_conn"] == "connected"
    print(f"测试1使用资源时间戳: {expensive_resource['timestamp']}")

def test_db_query_2(expensive_resource):
    assert expensive_resource["db_conn"] == "connected"
    print(f"测试2使用资源时间戳: {expensive_resource['timestamp']}")  # 时间戳相同,说明是同一个资源实例

在这个例子中,两个测试函数共享同一个 expensive_resource 实例,避免了重复初始化,大大提升了测试速度。

4.3 conftest.py:共享fixture的利器

当你的fixture需要在多个测试文件中共用时,不应该在每个文件里重复定义。pytest提供了一个特殊的文件 conftest.py 。你可以将公共的fixture放在项目根目录或任何子目录的 conftest.py 中,该目录及其所有子目录下的测试文件都能自动使用这些fixture。

项目结构示例:

pytest-automation-starter/
├── conftest.py          # 根目录conftest,全局可用
├── tests/
│   ├── conftest.py      # tests目录下的conftest,仅对tests内文件可用
│   ├── test_api.py
│   └── test_web.py
└── utils/
    └── test_utils.py    # 也能使用根目录conftest.py中的fixture

根目录 conftest.py 内容示例:

# conftest.py
import pytest
import requests
from typing import Dict, Any

@pytest.fixture(scope="session")
def base_url() -> str:
    """返回基础测试URL,session级别只获取一次"""
    return "https://jsonplaceholder.typicode.com"

@pytest.fixture
def api_client(base_url: str) -> requests.Session:
    """创建一个配置好的requests会话,function级别,每个测试独立"""
    session = requests.Session()
    session.headers.update({'Content-Type': 'application/json'})
    # 可以在这里配置超时、重试、认证等
    session.base_url = base_url
    yield session
    session.close()  # 测试结束后关闭会话

现在,在 tests/test_api.py 中,你可以直接使用 api_client base_url 这两个fixture,无需导入。

常见问题与排查 Q: 我的测试函数里声明了fixture参数,但运行时报错 FixtureNotFound A: 首先检查fixture名字是否拼写错误。其次,确认定义该fixture的 conftest.py 文件位置是否正确。fixture的查找范围是从测试文件所在目录向上逐级查找 conftest.py ,直到根目录。确保你的fixture定义在正确的 conftest.py 中,并且该文件在查找路径上。

Q: yield和return在fixture里有什么区别? A: 使用 return 的fixture没有teardown能力。 yield 允许你在返回数据给测试后,还能执行清理代码。如果不需要清理,用 return 也可以,但更推荐使用 yield 以保持模式统一,即使暂时没有清理逻辑。

5. 参数化测试:用一份代码覆盖多种场景

在自动化测试中,我们经常需要用不同的输入数据测试同一个逻辑。如果为每组数据都写一个测试函数,会导致代码大量重复。pytest的 @pytest.mark.parametrize 装饰器完美解决了这个问题。

5.1 基础参数化:测试函数

# tests/test_parametrize.py
import pytest

# 定义一个简单的函数用于测试
def is_even(n):
    return n % 2 == 0

# 使用parametrize装饰器
@pytest.mark.parametrize("test_input, expected", [
    (2, True),   # 第一组数据:test_input=2, expected=True
    (3, False),  # 第二组数据
    (0, True),
    (-4, True),
    (-7, False),
])
def test_is_even(test_input, expected):
    """多组数据测试is_even函数"""
    assert is_even(test_input) == expected
    # pytest会为每组数据生成一个独立的测试用例,并单独报告成功或失败

运行 pytest -v tests/test_parametrize.py ,你会看到5个独立的测试用例被执行,每个用例对应一组参数。

5.2 参数化与fixture结合

参数化也可以和fixture强强联合,实现更动态的数据生成。

import pytest

@pytest.fixture(params=[("user1", "pass1"), ("user2", "pass2"), ("admin", "admin123")])
def login_credentials(request):
    """一个fixture,本身被参数化,会生成多个实例"""
    username, password = request.param
    return {"username": username, "password": password}

def test_login_with_fixture_param(login_credentials):
    """这个测试会运行三次,每次使用fixture生成的一组凭证"""
    creds = login_credentials
    # 模拟登录逻辑...
    print(f"尝试登录用户: {creds['username']}")
    assert isinstance(creds['username'], str)
    assert isinstance(creds['password'], str)

5.3 实战:参数化接口测试

结合 requests 和参数化,我们可以轻松测试一个接口的多种输入输出情况。

# tests/test_parametrize_api.py
import pytest
import requests

# 假设我们有一个用户查询接口,根据用户ID返回用户信息
USER_SERVICE_URL = "https://jsonplaceholder.typicode.com/users"

@pytest.mark.parametrize("user_id, expected_name, expected_status", [
    (1, "Leanne Graham", 200),
    (2, "Ervin Howell", 200),
    (11, None, 404),  # 不存在的用户ID,期望404
])
def test_get_user_by_id(user_id, expected_name, expected_status):
    """参数化测试用户查询接口"""
    response = requests.get(f"{USER_SERVICE_URL}/{user_id}")
    
    assert response.status_code == expected_status
    
    if expected_status == 200:
        # 成功时验证返回的用户名
        data = response.json()
        assert data["name"] == expected_name
        assert data["id"] == user_id
    else:
        # 失败时验证返回体为空或包含错误信息
        # 这里根据实际API设计来断言
        assert response.json() == {}  # 示例,实际API可能返回错误信息

注意事项 :参数化测试虽然强大,但要避免过度使用。如果参数组合爆炸(比如从外部文件读取上百条测试数据),会导致测试套件运行时间过长。一个最佳实践是:将核心逻辑、边界条件、典型错误场景进行参数化,而将大量、重复的验证性测试数据放在单独的数据驱动测试中(例如使用 pytest @pytest.mark.from_csv 插件或自定义fixture读取外部文件)。

6. 断言与异常测试:不仅仅是assert

pytest的断言系统非常强大,它重写了Python的标准 assert 语句,在断言失败时能提供极其详细的上下文信息,这是它比unittest等框架更友好的地方。

6.1 智能断言与上下文信息

def test_advanced_assertion():
    list1 = [1, 2, 3, 4, 5]
    list2 = [1, 2, 0, 4, 5]
    
    # 当这个断言失败时,pytest会清晰地显示出list1和list2的差异
    assert list1 == list2
    
    dict1 = {'a': 1, 'b': 2, 'c': {'x': 10, 'y': 20}}
    dict2 = {'a': 1, 'b': 2, 'c': {'x': 10, 'y': 99}}
    # 对于复杂数据结构,差异对比更是一目了然
    assert dict1 == dict2

运行失败的测试,pytest会输出类似这样的信息,直接告诉你哪里不一样:

>       assert list1 == list2
E       assert [1, 2, 3, 4, 5] == [1, 2, 0, 4, 5]
E         At index 2 diff: 3 != 0
E         Use -v to see the full diff

6.2 测试异常:确保错误被正确抛出

在测试中,我们不仅需要测试正常路径,还要测试当输入非法或状态异常时,代码是否能按预期抛出异常。pytest提供了 pytest.raises 上下文管理器来优雅地处理这种情况。

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b

def test_divide_normal():
    assert divide(10, 2) == 5

def test_divide_by_zero():
    """测试当除数为零时,是否抛出ValueError异常"""
    with pytest.raises(ValueError) as exc_info:
        divide(10, 0)
    
    # 可以进一步断言异常的具体信息
    assert str(exc_info.value) == "除数不能为零"
    # exc_info是一个ExceptionInfo对象,可以通过.value获取异常实例
    assert exc_info.type is ValueError

def test_divide_by_zero_message():
    """另一种写法,直接匹配异常信息"""
    with pytest.raises(ValueError, match="除数不能为零"):
        divide(10, 0)

pytest.raises 会捕获代码块中抛出的异常。如果代码块没有抛出异常,或者抛出的异常类型不匹配,测试就会失败。这确保了我们的错误处理逻辑是健全的。

6.3 使用pytest-assume进行软断言

在UI自动化或集成测试中,一个测试用例可能包含多个检查点。传统的 assert 在第一个失败点就会终止当前测试,导致后面的检查点无法执行。 pytest-assume 插件提供了“软断言”功能,允许一个测试用例中所有断言都执行完毕,最后再报告哪些失败了。

# 首先安装插件
pip install pytest-assume
# tests/test_soft_assert.py
import pytest
import pytest_assume

def test_form_validation_with_soft_assert():
    # 模拟一个表单提交后,需要验证多个字段
    response = {
        "username": "john_doe",
        "email": "john@example",  # 邮箱格式错误
        "age": 15,                # 年龄未满18岁
        "agreed_to_terms": True
    }
    
    # 使用pytest.assume代替assert
    pytest.assume(response["username"] == "john_doe", "用户名校验失败")
    pytest.assume("@" in response["email"] and "." in response["email"], "邮箱格式校验失败")
    pytest.assume(response["age"] >= 18, "年龄必须满18岁")
    pytest.assume(response["agreed_to_terms"] is True, "必须同意条款")
    
    # 即使中间有断言失败,后面的断言依然会执行
    # 测试结束后,所有失败的断言会一起报告

运行这个测试,你会看到所有失败的断言都被收集并报告出来,而不是在第一个失败点就停止。这对于需要全面了解一个复杂操作后所有状态是否正确的场景非常有用。

7. 测试报告与结果分析:让结果一目了然

测试执行完毕后,清晰直观的报告对于分析问题、追踪进度至关重要。pytest本身提供了丰富的命令行输出,但我们可以通过插件生成更美观、信息更丰富的报告。

7.1 生成HTML报告(pytest-html)

我们之前已经安装了 pytest-html 插件,现在来使用它。

方法一:命令行参数

# 生成HTML报告到指定文件
pytest --html=report.html

# 同时生成一个独立的CSS文件(便于分享)
pytest --html=report.html --self-contained-html

方法二:通过pytest.ini配置文件(推荐) 在项目根目录创建 pytest.ini 文件,统一管理pytest配置。

# pytest.ini
[pytest]
# addopts 参数可以指定每次运行pytest时自动添加的默认选项
addopts = -v --html=./reports/pytest_report.html --self-contained-html

# 指定测试文件/目录的查找位置(非必须)
testpaths = tests

# 自定义标记,防止未注册的标记引发警告
markers =
    smoke: 冒烟测试
    regression: 回归测试
    slow: 运行缓慢的测试

现在,每次直接运行 pytest ,都会自动在 ./reports 目录下生成一个包含所有详细结果的HTML报告。报告里包含了测试通过/失败/跳过的数量、每个测试的执行时长、失败测试的详细错误信息和回溯栈,甚至还能包含测试期间的 stdout 输出(通过添加 --capture=sys -s 参数)。

7.2 生成Allure报告(更强大的选择)

对于企业级项目或需要更炫酷、更结构化报告的场景,Allure是一个行业标准的选择。它生成的报告支持用例分层、附件(截图、日志)、步骤描述、严重等级划分等。

安装与配置:

# 1. 安装pytest的Allure适配器
pip install allure-pytest

# 2. 还需要安装Allure命令行工具(这是一个Java程序)
# macOS: brew install allure
# Windows:  scoop install allure 或从官网下载zip包并配置PATH
# Linux: 参考官网文档

配置pytest生成Allure结果文件: 更新 pytest.ini 或在命令行指定:

pytest --alluredir=./allure-results

这不会直接生成HTML报告,而是生成一堆 .json 文件在 allure-results 目录中。

生成并查看HTML报告:

# 根据结果文件生成HTML报告
allure generate ./allure-results -o ./allure-report --clean

# 打开生成的HTML报告
allure open ./allure-report

在测试用例中丰富Allure报告:

import allure
import pytest

@allure.epic("用户管理模块")
@allure.feature("用户登录功能")
class TestUserLogin:
    
    @allure.story("使用正确用户名密码登录")
    @allure.title("验证成功登录后返回用户令牌")
    @allure.severity(allure.severity_level.BLOCKER)  # 阻塞级缺陷
    @allure.description("""
    这是一个详细的测试描述。
    步骤:
    1. 准备有效的用户名和密码
    2. 调用登录接口
    3. 验证返回状态码为200
    4. 验证返回体包含token字段
    """)
    def test_login_success(self):
        with allure.step("步骤1: 准备测试数据"):
            username = "test_user"
            password = "secure_pass"
        
        with allure.step("步骤2: 调用登录接口"):
            # 模拟接口调用
            response = {"status": 200, "token": "abc123xyz"}
            allure.attach(str(response), name="接口响应", attachment_type=allure.attachment_type.JSON)
        
        with allure.step("步骤3: 验证响应"):
            assert response["status"] == 200
            assert "token" in response
        
        with allure.step("步骤4: 附加自定义截图(模拟)"):
            # 如果是UI测试,这里可以附加截图
            allure.attach.file('./dummy_screenshot.png', name='登录成功页面截图', attachment_type=allure.attachment_type.PNG)

Allure报告会将这些装饰器信息(epic, feature, story, step等)组织成一个层次分明的树状结构,并展示步骤详情和附件,极大地提升了测试结果的可读性和可追溯性。

实操心得 :对于团队协作和CI/CD集成,我通常这样安排报告策略:

  1. 本地开发 :使用 -v -s 参数,在终端查看详细输出和打印信息,快速调试。
  2. 本地完整运行 :使用 pytest-html 生成一个简单的HTML报告,快速浏览整体结果。
  3. CI/CD流水线 :使用 allure-pytest 生成结果文件,在Jenkins/GitLab CI等工具中集成Allure插件,自动生成并发布链接到流水线的Allure报告,供整个团队查看历史趋势和详细失败信息。

8. 高级配置与最佳实践

掌握了基础用法后,了解一些高级配置和团队协作的最佳实践,能让你的pytest项目更加健壮和高效。

8.1 灵活的配置文件pytest.ini

pytest.ini 是pytest的主要配置文件,除了上面提到的 addopts markers ,还有其他常用配置:

# pytest.ini
[pytest]
addopts = 
    -v
    --strict-markers  # 严格检查标记,使用未注册的标记会报错
    --tb=short        # 设置失败回溯的详细程度:short, line, no, auto, long...
    --disable-warnings  # 禁用警告输出,让报告更干净
    -p no:warnings    # 另一种禁用警告的方式

# 指定测试文件命名模式(默认已包含,可自定义)
python_files = test_*.py *_test.py

# 指定测试类/函数命名模式
python_classes = Test* *Test
python_functions = test_*

# 自定义测试目录(pytest会递归搜索这些目录)
testpaths = 
    tests
    integration_tests
    e2e_tests

# 定义自定义标记(非常重要!用于分类和筛选测试)
markers =
    smoke: 快速冒烟测试,验证核心功能
    regression: 全量回归测试
    api: 接口测试
    ui: UI自动化测试
    slow: 运行缓慢的测试,通常不纳入CI快速流水线
    nightly: 每晚执行的测试套件

8.2 测试标记(Mark)与选择性执行

使用 @pytest.mark.标记名 装饰器给你的测试打上标签,然后可以灵活地选择执行。

# tests/test_markers.py
import pytest
import time

@pytest.mark.smoke
@pytest.mark.api
def test_quick_api_check():
    """冒烟测试中的API检查"""
    assert True

@pytest.mark.regression
@pytest.mark.ui
@pytest.mark.slow  # 标记为慢测试
def test_complex_ui_flow():
    """回归测试中的复杂UI流程,执行较慢"""
    time.sleep(2)  # 模拟耗时操作
    assert True

@pytest.mark.nightly
def test_data_migration():
    """每晚执行的数据迁移测试"""
    assert True

命令行筛选执行:

# 只运行冒烟测试
pytest -m smoke

# 运行冒烟测试或API测试(逻辑或)
pytest -m "smoke or api"

# 运行既是回归测试又是UI测试的用例(逻辑与)
pytest -m "regression and ui"

# 运行除了慢测试以外的所有用例
pytest -m "not slow"

# 运行标记为nightly的测试,并输出详细结果
pytest -m nightly -v

8.3 测试跳过(skip)与条件跳过(skipif)

有时某些测试在某些条件下不应该运行,比如外部服务不可用、特定平台不兼容等。

import pytest
import sys

@pytest.mark.skip(reason="此功能尚未实现,跳过测试")
def test_unimplemented_feature():
    assert False

# 根据Python版本条件跳过
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8或更高版本")
def test_feature_requires_py38():
    # 使用了Python 3.8的walrus运算符等特性
    assert (x := 5) == 5

# 根据环境变量条件跳过
import os
API_URL = os.getenv("API_URL")

@pytest.mark.skipif(not API_URL, reason="需要设置API_URL环境变量")
def test_external_api():
    # 测试依赖于外部API
    pass

8.4 测试夹具(Fixture)的自动使用(autouse)

有些fixture(例如日志初始化、临时目录创建)需要在每个测试中自动使用,而不必显式声明为参数。这时可以使用 autouse=True

import pytest
import tempfile
import os

@pytest.fixture(autouse=True, scope="function")
def setup_logging():
    """每个测试函数自动运行,初始化日志(示例)"""
    print("\n=== 测试开始:设置日志 ===")
    yield
    print("=== 测试结束:清理日志 ===")

@pytest.fixture(scope="session", autouse=True)
def global_temp_dir():
    """整个测试会话自动创建一个临时目录,所有测试共用"""
    temp_dir = tempfile.mkdtemp(prefix="pytest_auto_")
    print(f"\n>>> 创建会话级临时目录: {temp_dir}")
    yield temp_dir
    # 测试会话结束后清理
    import shutil
    shutil.rmtree(temp_dir)
    print(f">>> 清理会话级临时目录: {temp_dir}")

def test_something():
    # 这个测试会自动调用上面两个autouse fixture
    print("执行测试逻辑...")
    assert True

最佳实践建议

  1. 项目结构 :保持清晰。例如: tests/unit/ , tests/integration/ , tests/e2e/ 。为不同类型的测试准备不同的 conftest.py
  2. fixture作用域 :谨慎选择。默认用 function ,对于昂贵的资源(数据库连接、浏览器启动)考虑 session module 级,并用 autouse=False 在需要的地方显式请求。
  3. 测试数据 :与测试代码分离。将测试数据(尤其是用于参数化的大量数据)放在JSON、YAML或CSV文件中,通过fixture读取。
  4. 避免测试依赖 :每个测试都应该是独立的、可重复的。不要依赖其他测试的执行顺序或状态。使用 pytest-randomly 插件可以强制随机执行顺序,帮你发现隐藏的依赖。
  5. 命名清晰 :测试函数名应该描述清楚它在测试什么,例如 test_login_with_invalid_password_fails test_login_2 好得多。
  6. 及时清理 :fixture中通过 yield finalizer 确保资源被正确释放(关闭文件、数据库连接、浏览器等)。

从一行简单的 assert 到构建一个支持多环境、数据驱动、并发执行、拥有精美报告的自动化测试项目,pytest提供了一套完整而优雅的工具链。它的学习路径是平滑的,你可以从今天写的这个最简单的测试文件开始,逐步引入fixture、参数化、插件,最终构建出适应你项目复杂度的测试体系。记住,好的测试框架不应该成为你的负担,而应该像一位得力的助手,让编写和维护测试代码成为一种愉悦的体验。

更多推荐