1. 项目概述:为什么选择 Pytest 与 PyCharm 的组合?

如果你刚开始接触 Python 测试,或者厌倦了 unittest 那略显繁琐的 setUp tearDown assert 语句,那么 Pytest 绝对是一个能让你“真香”的框架。它用起来有多爽?简单来说,它允许你用最接近自然语言的 assert 来写断言,自动发现测试用例,并且拥有极其丰富的插件生态。而 PyCharm,作为 JetBrains 出品的 Python IDE,其智能提示、代码导航和集成的测试运行器,能让开发和测试的体验无缝衔接。今天,我就带你从零开始,在 Python 3.8 环境下,用 PyCharm 搭建一个功能完备、结构清晰的 Pytest 测试框架。这个框架不仅适用于单元测试,其良好的扩展性也能轻松支撑接口自动化、Web UI 自动化(如结合 Selenium)等更复杂的场景。无论你是测试新手想系统入门,还是开发同学想提升代码质量,这套组合拳都能让你事半功倍。

2. 环境准备与核心工具安装

搭建环境是第一步,也是最容易出问题的一步。很多人在这里卡住,不是因为步骤复杂,而是因为环境冲突或版本不匹配。我们一步步来,确保你的基础环境是干净、稳定的。

2.1 Python 3.8 的安装与配置

为什么选择 Python 3.8?这是一个在稳定性和新特性之间取得很好平衡的版本。它完全支持 Pytest 的所有现代功能,同时避免了 Python 3.7 之前的一些兼容性问题,也比 3.9+ 的版本在部分老库的兼容性上更友好。

首先,去 Python 官网下载安装包。这里有个关键点: 务必勾选“Add Python 3.8 to PATH” 。这个选项会将 Python 和 pip 添加到系统环境变量,让你能在任何命令行窗口直接使用 python pip 命令。如果不勾选,后续手动配置会比较麻烦。

安装完成后,打开命令行(CMD 或 PowerShell),验证安装是否成功:

python --version
pip --version

应该分别显示 Python 3.8.x pip 20.x.x 类似的版本信息。

接下来,我强烈建议你为这个测试项目创建一个独立的虚拟环境。虚拟环境可以理解为项目专属的“沙箱”,它能隔离不同项目所需的第三方库,避免版本冲突。使用 Python 自带的 venv 模块来创建:

# 切换到你的项目目录,例如 D:\Projects
cd D:\Projects
# 创建一个名为 `pytest_env` 的虚拟环境
python -m venv pytest_env

创建完成后,激活虚拟环境:

  • Windows (CMD): pytest_env\Scripts\activate.bat
  • Windows (PowerShell): pytest_env\Scripts\Activate.ps1 (可能需要先执行 Set-ExecutionPolicy RemoteSigned 允许脚本运行)
  • macOS/Linux: source pytest_env/bin/activate

激活后,命令行提示符前会出现 (pytest_env) 字样,表示你已进入该虚拟环境。之后所有包的安装都只影响这个环境。

2.2 PyCharm 的安装与基础设置

前往 JetBrains 官网下载 PyCharm 社区版(Community)。社区版对于 Python 开发和测试来说功能已经完全足够,并且免费。专业版(Professional)主要多了对 Web 框架(如 Django)、数据库工具和科学计算模式的支持,对于纯测试框架搭建并非必需。

安装过程基本一路“Next”即可。安装完成后首次启动,我们需要进行几项关键配置:

  1. 配置 Python 解释器 :这是连接 PyCharm 和你刚创建的虚拟环境的关键一步。

    • 打开 PyCharm,创建新项目( New Project )。
    • Location 选择你的项目路径(例如 D:\Projects\my_pytest_framework )。
    • 重点来了 :在 Python Interpreter 部分,不要选择“New environment”,而是选择“Previously configured interpreter”。点击右侧的 ... 按钮,然后选择 Add Interpreter -> Add Local Interpreter
    • 在弹出的窗口中,选择 Virtualenv Environment -> Existing environment ,然后导航到你刚才创建的 pytest_env 目录下的 Scripts (Windows)或 bin (macOS/Linux)文件夹,选择 python.exe (或 python )文件。
    • 点击确定。这样,PyCharm 就会使用我们虚拟环境中的 Python 和 pip 来管理项目依赖。
  2. 优化测试运行配置 :为了让 PyCharm 更好地识别和运行 Pytest 测试,我们需要告诉它默认的测试运行器。

    • 进入 File -> Settings (Windows) 或 PyCharm -> Preferences (macOS)。
    • 导航到 Tools -> Python Integrated Tools
    • Testing 部分,将 Default test runner Unittests 改为 pytest
    • 这样,当你右键点击测试文件或测试函数时,“Run”和“Debug”选项就会默认使用 Pytest 来执行,并能在 PyCharm 的“Run”工具窗口看到漂亮的 Pytest 风格输出。

注意 :有些教程会教你在 PyCharm 中直接创建虚拟环境,但我更推荐先在命令行创建好再关联。这样做的好处是,你对虚拟环境的控制力更强,比如可以方便地用命令行批量安装依赖,或者在无 GUI 的服务器环境下复现相同环境。这是一种更“工程化”的习惯。

3. Pytest 框架核心组件与项目结构设计

环境搭好了,我们开始设计框架本身。一个好的项目结构是后续可维护性和扩展性的基石。很多人一开始把所有测试用例、数据和逻辑混在一个文件里,随着用例增多,很快就会变得难以管理。

3.1 初始化项目与安装核心依赖

在 PyCharm 中打开你的项目,首先在项目根目录下创建一个 requirements.txt 文件。这个文件用于记录项目所有依赖包及其版本,方便在任何地方一键复现环境。初始内容如下:

pytest>=6.2.5
pytest-html>=3.1.1
pytest-xdist>=2.4.0
pytest-rerunfailures>=10.2
pytest-ordering>=0.6
  • pytest : 核心框架。
  • pytest-html : 生成美观的 HTML 测试报告。
  • pytest-xdist : 实现测试用例的分布式执行,加速测试过程。
  • pytest-rerunfailures : 对失败的测试用例进行重跑,应对网络抖动等偶发问题。
  • pytest-ordering : 控制测试用例的执行顺序(谨慎使用,测试最好独立)。

然后,在 PyCharm 的终端(Terminal)中,确保虚拟环境已激活,执行安装:

pip install -r requirements.txt

如果下载慢,可以临时使用国内镜像源: pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

3.2 设计清晰的项目目录结构

接下来,创建以下目录结构。你可以直接在 PyCharm 的项目视图中右键新建目录和文件。

my_pytest_framework/
├── requirements.txt          # 依赖清单
├── pytest.ini               # Pytest 主配置文件
├── conftest.py              # 全局夹具和钩子函数定义
├── common/                  # 公共模块
│   ├── __init__.py
│   ├── logger.py            # 日志模块
│   └── config.py            # 配置管理(读取yaml/ini等)
├── test_cases/              # 测试用例集
│   ├── __init__.py
│   ├── test_api/            # 接口测试用例
│   │   ├── __init__.py
│   │   └── test_user_api.py
│   ├── test_web/            # Web UI测试用例
│   │   ├── __init__.py
│   │   └── test_login.py
│   └── test_unit/           # 单元测试用例
│       ├── __init__.py
│       └── test_calculator.py
├── test_data/               # 测试数据管理
│   ├── api_data.yaml
│   └── web_data.json
├── utils/                   # 工具函数
│   ├── __init__.py
│   ├── http_client.py       # 封装的HTTP请求客户端
│   └── file_reader.py       # 文件读取工具
├── reports/                 # 测试报告输出目录(.gitignore忽略)
│   └── assets/              # 报告所需的静态资源
└── logs/                    # 日志文件输出目录(.gitignore忽略)

为什么这样设计?

  • 按功能/层级分离 common 放跨模块共享的代码; test_cases 按测试类型分目录,用例文件以 test_ 开头,Pytest 才能自动发现; test_data 独立存放,实现数据与脚本分离; utils 放可复用的辅助函数。
  • conftest.py 的特殊性 :这个文件是 Pytest 的“魔法”文件。它可以放在任何目录下,其作用域是该目录及其所有子目录。根目录的 conftest.py 里定义的夹具(fixture)对整个项目生效。我们用它来定义那些最常用、最基础的夹具,比如驱动初始化、日志对象、配置读取等。
  • pytest.ini 的作用 :这是 Pytest 的配置文件,用于定义默认的命令行选项、标记(marks)以及自定义一些行为。它能让你在运行 pytest 命令时不用每次都敲一长串参数。

4. 核心配置文件与基础夹具(Fixture)编写

有了结构,我们来填充核心内容。 pytest.ini conftest.py 是框架的“大脑”和“中枢神经”。

4.1 配置 pytest.ini 文件

在项目根目录创建 pytest.ini ,内容如下:

[pytest]
# 指定测试用例的查找路径和文件名模式
testpaths = test_cases
python_files = test_*.py
python_classes = Test*
python_functions = test_*

# 定义自定义标记,用于分类运行测试
markers =
    smoke: 冒烟测试用例
    regression: 回归测试用例
    slow: 运行缓慢的测试用例

# 添加默认的命令行参数
addopts =
    -v                    # 显示详细结果
    --strict-markers      # 严格检查标记,未注册的标记会报错
    --html=reports/report.html  # 生成HTML报告
    --self-contained-html # 将CSS等嵌入HTML,使报告单文件可独立查看
    --capture=sys         # 捕获输出,测试失败时打印
    -p no:warnings        # 不显示警告信息(可选,保持输出整洁)

# 配置日志(需要与你的logger模块配合)
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)s] %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S

这个配置做了几件事:

  1. 告诉 Pytest 去 test_cases 目录下找以 test_ 开头的 .py 文件,并识别其中以 Test 开头的类和以 test_ 开头的函数作为测试用例。
  2. 定义了 smoke regression slow 三个标记。你可以在测试用例上用 @pytest.mark.smoke 来标记它,然后通过 pytest -m smoke 只运行冒烟测试。
  3. addopts 里的参数会在每次执行 pytest 命令时自动生效。比如这里配置了自动生成 HTML 报告到 reports 目录。

4.2 编写 conftest.py 定义全局夹具

夹具(Fixture)是 Pytest 的灵魂,用于提供测试所需的环境准备和清理工作,比如数据库连接、临时文件、WebDriver 实例等。我们在根目录的 conftest.py 中定义最通用的夹具。

import pytest
import logging
import sys
from datetime import datetime
from common.logger import get_logger  # 假设我们有一个自定义的日志模块
from common.config import Config      # 假设我们有一个配置管理模块

# 1. 获取日志记录器的夹具
@pytest.fixture(scope="session", autouse=True)
def logger():
    """为整个测试会话提供一个日志记录器"""
    log = get_logger(name="pytest_framework", level=logging.INFO)
    yield log
    # 测试会话结束后,可以在这里添加日志归档等清理操作
    log.info("测试会话结束于 %s", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))

# 2. 读取配置的夹具
@pytest.fixture(scope="session")
def config():
    """加载全局配置,如基础URL、数据库连接信息等"""
    cfg = Config("config.ini")  # 或 config.yaml
    return cfg

# 3. 模拟一个Web Driver初始化的夹具(示例)
@pytest.fixture(scope="function")  # 每个测试函数都新建一个driver
def browser_driver(config, logger):
    """为Web UI测试提供浏览器驱动实例"""
    from selenium import webdriver
    browser_type = config.get("browser", "chrome")
    driver = None
    if browser_type.lower() == "chrome":
        options = webdriver.ChromeOptions()
        options.add_argument("--headless")  # 无头模式,不打开GUI,适合CI环境
        options.add_argument("--disable-gpu")
        options.add_argument("--window-size=1920,1080")
        driver = webdriver.Chrome(options=options)
    elif browser_type.lower() == "firefox":
        # ... Firefox 配置
        pass
    else:
        raise ValueError(f"不支持的浏览器类型: {browser_type}")

    logger.info(f"初始化 {browser_type} 浏览器驱动")
    yield driver  # 将driver对象提供给测试用例使用
    # 测试函数执行完毕后,执行清理
    if driver:
        driver.quit()
        logger.info(f"关闭 {browser_type} 浏览器驱动")

# 4. 模拟一个HTTP客户端的夹具(示例)
@pytest.fixture(scope="class")  # 每个测试类共享一个client
def api_client(config, logger):
    """为API测试提供预配置的HTTP客户端"""
    from utils.http_client import HttpClient
    base_url = config.get("api", "base_url")
    client = HttpClient(base_url=base_url, logger=logger)
    # 可以在这里添加通用的请求头,如认证token
    # client.set_default_headers({"Authorization": f"Bearer {token}"})
    yield client
    # 可选的清理工作,如关闭持久连接
    client.close()

# 5. 处理测试失败时截图的夹具(钩子函数形式)
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """在测试用例执行后生成报告,可用于失败时截图"""
    outcome = yield
    rep = outcome.get_result()
    # 如果测试失败,且该测试用例使用了 `browser_driver` 夹具
    if rep.when == "call" and rep.failed:
        for fixture_name in item.fixturenames:
            if "browser_driver" in fixture_name:
                driver = item.funcargs[fixture_name]
                if driver:
                    try:
                        # 截图并保存
                        screenshot_dir = "./reports/screenshots/"
                        os.makedirs(screenshot_dir, exist_ok=True)
                        screenshot_path = os.path.join(screenshot_dir, f"{item.name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")
                        driver.save_screenshot(screenshot_path)
                        # 将截图路径添加到HTML报告中
                        if hasattr(rep, 'extra'):
                            from pytest_html import extras
                            rep.extra.append(extras.image(screenshot_path))
                            rep.extra.append(extras.html(f'<div><a href="{screenshot_path}" target="_blank">查看截图</a></div>'))
                    except Exception as e:
                        print(f"截图失败: {e}")

夹具(Fixture)的核心理解

  • scope 参数 :定义了夹具的作用域和生命周期。
    • function (默认):每个测试函数运行一次。
    • class :每个测试类运行一次,该类中的所有测试方法共享这个夹具实例。
    • module :每个 .py 文件运行一次。
    • session :整个 Pytest 执行过程(一次命令行调用)只运行一次。像 logger config 这种全局资源非常适合用 session 作用域。
  • autouse=True :表示该夹具会自动应用于所有符合条件的测试用例,无需在测试函数参数中显式声明。谨慎使用,避免不必要的开销。
  • yield 语句 :这是夹具提供数据和执行清理的关键。 yield 之前的代码是“设置”阶段,返回值通过 yield 传递给测试用例。测试用例执行完毕后,会回到 yield 这里,执行其后的代码,即“清理”阶段。这比传统的 setup/teardown 更清晰。
  • 钩子函数(Hook) :如 pytest_runtest_makereport ,允许你在 Pytest 执行的特定生命周期插入自定义逻辑,功能非常强大。

5. 编写与组织测试用例

框架搭好了,现在我们来写一些实际的测试用例,看看如何利用我们设计的结构和夹具。

5.1 单元测试示例:测试一个计算器类

首先在 test_cases/test_unit/ 下创建 test_calculator.py 。假设我们有一个简单的计算器类 Calculator (可以定义在同目录或 utils 下)。

# 被测试的类 (可以放在 ../utils/calculator.py)
class Calculator:
    def add(self, a, b):
        return a + b
    def subtract(self, a, b):
        return a - b
    def multiply(self, a, b):
        return a * b
    def divide(self, a, b):
        if b == 0:
            raise ValueError("除数不能为零")
        return a / b

# 测试用例文件 test_calculator.py
import pytest
from utils.calculator import Calculator

class TestCalculator:
    """测试计算器类"""

    # 在每个测试方法前创建新的Calculator实例
    @pytest.fixture(autouse=True)
    def setup_calculator(self):
        self.calc = Calculator()
        yield
        # 如果需要,可以在这里清理资源
        self.calc = None

    # 标记为冒烟测试
    @pytest.mark.smoke
    def test_add(self):
        """测试加法"""
        result = self.calc.add(2, 3)
        assert result == 5, f"预期 2+3=5,实际得到 {result}"
        # Pytest的assert非常强大,可以直接比较列表、字典等复杂对象
        assert self.calc.add(-1, 1) == 0
        assert self.calc.add(0, 0) == 0

    def test_subtract(self):
        """测试减法"""
        assert self.calc.subtract(5, 3) == 2
        assert self.calc.subtract(0, 5) == -5

    # 参数化测试:用一组数据测试同一个逻辑
    @pytest.mark.parametrize("a, b, expected", [
        (6, 3, 2),
        (10, 2, 5),
        (0, 1, 0),
        (-4, 2, -2),
    ])
    def test_divide_normal(self, a, b, expected):
        """测试正常除法"""
        result = self.calc.divide(a, b)
        assert result == expected

    def test_divide_by_zero(self):
        """测试除零异常"""
        with pytest.raises(ValueError) as exc_info:
            self.calc.divide(5, 0)
        # 可以进一步断言异常信息
        assert "除数不能为零" in str(exc_info.value)

要点解析

  • 测试类与测试方法 :类名以 Test 开头,方法以 test_ 开头。Pytest 都能发现。
  • 类级别的夹具 setup_calculator 夹具使用 autouse=True ,这个类里的每个测试方法执行前都会先执行它,创建一个新的 Calculator 实例。这比在每个方法里写 self.calc = Calculator() 更清晰,也便于未来扩展(比如需要更复杂的初始化逻辑)。
  • 标记(Mark) @pytest.mark.smoke test_add 标记为冒烟测试。之后可以用 pytest -m smoke 单独运行它。
  • 参数化测试 @pytest.mark.parametrize 是 Pytest 的杀手锏之一。它允许你用多组数据驱动同一个测试函数,极大减少了重复代码。上面的例子用四组数据测试了 divide 方法。
  • 异常断言 pytest.raises 上下文管理器用于断言代码块抛出了预期的异常。 exc_info 对象包含了异常的详细信息,可以用于进一步验证。

5.2 接口测试示例:使用夹具中的 HTTP 客户端

假设我们在 conftest.py 中定义了 api_client 夹具。现在在 test_cases/test_api/ 下创建 test_user_api.py

import pytest
import allure  # 可选:使用Allure报告增强描述,需要额外安装 pytest-allure

class TestUserAPI:
    """用户相关API测试"""

    # 测试获取用户列表
    @pytest.mark.regression
    def test_get_user_list(self, api_client, logger):
        """测试获取用户列表接口"""
        # 使用夹具注入的 api_client 和 logger
        endpoint = "/api/users"
        logger.info(f"请求接口: GET {endpoint}")
        
        response = api_client.get(endpoint)
        
        # 断言状态码
        assert response.status_code == 200, f"预期状态码200,实际为 {response.status_code}"
        # 断言响应体结构
        data = response.json()
        assert isinstance(data, list), "响应体应该是一个用户列表"
        # 如果有特定业务逻辑,例如第一个用户必须有id和name字段
        if data:
            first_user = data[0]
            assert "id" in first_user
            assert "name" in first_user
            logger.info(f"成功获取到 {len(data)} 个用户")

    # 测试创建用户
    @pytest.mark.parametrize("user_data, expected_status", [
        ({"name": "Alice", "email": "alice@example.com"}, 201),
        ({}, 400),  # 缺失必填字段,预期返回400
        ({"name": "Bob"}, 400),  # 缺失email字段
    ])
    def test_create_user(self, api_client, user_data, expected_status):
        """测试创建用户接口,参数化验证不同输入"""
        endpoint = "/api/users"
        response = api_client.post(endpoint, json=user_data)
        
        assert response.status_code == expected_status
        if expected_status == 201:
            created_user = response.json()
            assert created_user["name"] == user_data["name"]
            assert "id" in created_user  # 创建成功后应返回id

    # 使用 allure 添加更丰富的报告描述(可选)
    @allure.feature("用户管理")
    @allure.story("删除用户")
    @allure.severity(allure.severity_level.CRITICAL)
    def test_delete_user(self, api_client, config):
        """测试删除用户接口(关键路径)"""
        # 可能先创建一个测试用户,获取其ID
        test_user = {"name": "TempUserForDelete"}
        create_resp = api_client.post("/api/users", json=test_user)
        if create_resp.status_code == 201:
            user_id = create_resp.json()["id"]
            
            delete_endpoint = f"/api/users/{user_id}"
            delete_resp = api_client.delete(delete_endpoint)
            
            assert delete_resp.status_code == 204  # 成功删除通常返回204 No Content
            
            # 验证用户已被删除:再次查询应返回404
            get_resp = api_client.get(f"/api/users/{user_id}")
            assert get_resp.status_code == 404

接口测试要点

  • 依赖注入 :测试函数通过参数 api_client logger 直接使用我们在 conftest.py 中定义的夹具,无需关心它们是如何初始化和清理的。这是依赖注入模式的典型应用,让测试逻辑非常干净。
  • 断言层次 :从 HTTP 状态码到响应体结构,再到具体的业务字段,层层递进地进行断言。
  • 测试数据分离 :参数化测试中的数据可以进一步外移到 test_data/ 目录下的 YAML 或 JSON 文件中,通过 @pytest.mark.parametrize 动态读取,实现彻底的数据驱动。
  • Allure 集成 :虽然 Pytest-html 报告不错,但 Allure 报告在美观度和信息组织上更胜一筹。通过 @allure 装饰器可以为测试用例添加功能模块、用户故事、严重等级等标签,生成非常专业的测试报告。这需要额外安装 pytest-allure 和 Allure 命令行工具。

6. 高级技巧、问题排查与最佳实践

框架跑起来之后,我们会遇到一些实际问题和优化需求。这部分分享一些我踩过坑后总结的经验。

6.1 测试用例的依赖、顺序与跳过

理想的单元测试应该是独立的、无状态的。但有时难免有特殊情况。

  • 处理测试依赖(慎用) :Pytest 不鼓励测试之间有依赖。如果实在需要(比如B接口依赖A接口创建的资源),可以通过 @pytest.mark.dependency() 装饰器来显式声明依赖关系(需要安装 pytest-dependency 插件)。但更好的做法是,每个测试自己准备所需的数据状态,或者在 setup_class / setup_module 夹具中准备公共前置条件。
  • 控制执行顺序(慎用) :Pytest 默认按文件名和测试函数名排序执行。使用 pytest-ordering 插件可以强行指定顺序( @pytest.mark.run(order=1) ),但这违背了测试独立性原则。仅在极少数情况下(如性能测试有严格顺序)使用。
  • 跳过测试 :使用 @pytest.mark.skip(reason="原因") 无条件跳过,或 @pytest.mark.skipif(condition, reason="原因") 在条件满足时跳过。例如:
    import sys
    @pytest.mark.skipif(sys.platform != "win32", reason="仅需在Windows上运行")
    def test_windows_specific_feature():
        pass
    

6.2 并发执行与失败重试

当测试用例成百上千时,串行执行会非常耗时。 pytest-xdist 插件可以让我们并行运行测试。

  • 基本使用 :安装后,使用 -n 参数指定并行进程数( auto 表示自动检测CPU核心数)。
    pytest -n auto --html=reports/report.html
    
  • 注意事项 :并行测试时,要确保测试用例之间没有资源冲突(如写入同一个文件、使用同一个端口)。夹具的作用域需要仔细考虑, session module 作用域的夹具在并行模式下可能会被多个进程共享或重复初始化,需要确保它们是线程/进程安全的。对于 function 作用域的夹具,通常没有问题。

pytest-rerunfailures 插件用于处理偶发性失败(如网络超时)。

  • 使用 :通过 --reruns 参数指定重试次数, --reruns-delay 指定重试间隔。
    pytest --reruns 3 --reruns-delay 2
    
  • 最佳实践 :不要滥用重试来掩盖真正的代码缺陷。它只应用于处理已知的、外部的、偶发的不稳定因素。

6.3 常见问题排查(FAQ)

  1. Pytest 找不到测试用例?

    • 检查文件名是否以 test_ 开头,或者类名以 Test 开头,函数名以 test_ 开头。
    • 检查 pytest.ini 中的 testpaths python_files 配置是否正确。
    • 在项目根目录下运行 pytest --collect-only 命令,查看 Pytest 发现了哪些测试项。
  2. 夹具(Fixture)未找到?

    • 确保夹具定义在测试文件本身、或该文件所在/上级目录的 conftest.py 中。
    • 检查夹具名称在测试函数参数中是否拼写正确。
    • 检查夹具的作用域(scope)。如果一个 session 作用域的夹具依赖于一个 function 作用域的夹具,会导致错误。
  3. 导入模块失败(ModuleNotFoundError)?

    • 确保你的项目根目录(包含 pytest.ini 的目录)在 Python 的模块搜索路径中。一个简单的方法是在根目录下创建一个空的 __init__.py 文件(使其成为一个包),或者在运行测试时确保当前工作目录是项目根目录。
    • 在 PyCharm 中,右键点击项目根目录 -> Mark Directory as -> Sources Root 。这会将此目录添加到 PyCharm 的源代码路径。
  4. 生成的 HTML 报告没有样式或图片?

    • 确保 pytest.ini addopts 包含了 --self-contained-html 参数,这样 CSS 会内嵌到 HTML 中。
    • 如果报告需要引用外部截图,确保截图路径是相对路径且能被报告文件访问。 pytest-html extras 功能可以帮助嵌入图片。
  5. 测试用例中有打印语句,但控制台没输出?

    • Pytest 默认会捕获所有标准输出(stdout/stderr),只在测试失败时显示。使用 -s 参数可以禁用捕获,看到所有打印信息: pytest -s
    • 或者,使用 logger.info() 来记录信息,并通过配置 log_cli = true 在控制台实时查看日志。

6.4 持续集成(CI)集成建议

一个成熟的测试框架最终要融入 CI/CD 流水线。这里有一些关键点:

  • 环境隔离 :在 CI 服务器(如 Jenkins、GitLab CI、GitHub Actions)上,每次构建都应在一个全新的虚拟环境或容器中安装依赖( pip install -r requirements.txt )。
  • 命令与参数 :CI 脚本中运行测试的命令应包含生成机器可读报告(如 JUnit XML)和人工可读报告(HTML)的参数。
    pytest -v --junitxml=reports/junit.xml --html=reports/report.html --self-contained-html
    
  • 结果收集 :配置 CI 工具收集 reports/junit.xml 文件,以便在流水线界面展示测试通过率、趋势图等。将 reports/report.html 作为构建产物存档,供随时查看。
  • 并行与重试 :在 CI 中充分利用 pytest-xdist 并行执行以缩短反馈时间。对于不稳定测试,可以配置 pytest-rerunfailures
  • 失败通知 :配置 CI 在测试失败时通过邮件、钉钉、Slack 等渠道通知相关人员。

从零搭建一个 Pytest 测试框架,远不止是安装一个包和写几个 assert 。它涉及到项目结构设计、配置管理、夹具的生命周期、测试数据的组织、报告的生成以及与整个开发流程的集成。这套基于 Python 3.8 和 PyCharm 的方案,提供了一个兼顾灵活性、可维护性和工程实践性的起点。你可以根据实际项目需求,轻松地引入 Page Object 模型来组织 UI 测试,或者集成 Requests、Selenium、Appium 等库来扩展测试能力。最重要的是,理解每个组件背后的“为什么”,这样你才能在其基础上进行有效的定制和优化,让它真正成为提升你和团队研发效率的利器。

更多推荐