【pytest】pytest详解-入门到精通

pytest详解

文章目录

  • pytest详解
    • [一、Pytest 语法概览](#一、Pytest 语法概览)
      • [1. **基本测试结构**](#1. 基本测试结构)
      • [2. **断言语法**](#2. 断言语法)
      • [3. **参数化测试**](#3. 参数化测试)
    • [二、配置文件 pytest.ini](#二、配置文件 pytest.ini)
      • [1. 基本配置](#1. 基本配置)
      • [2. 环境特定配置](#2. 环境特定配置)
    • [三、pytest.main() 函数详解](#三、pytest.main() 函数详解)
      • [1. **基本用法**](#1. 基本用法)
      • [2. **高级配置**](#2. 高级配置)
      • [3. **插件集成**](#3. 插件集成)
    • [四、YAML 用例配置](#四、YAML 用例配置)
      • [1. **测试数据配置**](#1. 测试数据配置)
      • [2. **YAML 加载器**](#2. YAML 加载器)
      • [3. **参数化使用**](#3. 参数化使用)
    • 五、请求和响应校验
      • [1. **Request 封装**](#1. Request 封装)
      • [2. **Response 校验器**](#2. Response 校验器)
      • [3. **测试中使用**](#3. 测试中使用)
    • [六、conftest.py 作用详解](#六、conftest.py 作用详解)
      • [1. **conftest.py 结构**](#1. conftest.py 结构)
      • [2. **conftest.py 层级结构**](#2. conftest.py 层级结构)
    • [七、Pytest 架构图 (Mermaid)](#七、Pytest 架构图 (Mermaid))
    • [八、Pytest 工作流程图](#八、Pytest 工作流程图)
    • 九、最佳实践总结
      • [1. **项目结构**](#1. 项目结构)
      • [2. **常用命令**](#2. 常用命令)
      • [3. **实用技巧**](#3. 实用技巧)

一、Pytest 语法概览

pytest是一个基于Python的测试框架,它使用简单的assert语句来进行断言,并且具有丰富的插件系统。其语法特点如下:

  • 测试文件以test_开头或_test结尾。
  • 测试类以Test开头,且不能有__init__方法。
  • 测试函数以test_开头。
  • 使用assert进行断言,例如:assert 1 == 1
python 复制代码
# test_example.py
def test_addition():
    assert 1 + 1 == 2

class TestMath:
    def test_multiplication(self):
        assert 2 * 2 == 4

1. 基本测试结构

python 复制代码
# test_sample.py
def test_addition():
    """基本测试函数"""
    assert 1 + 1 == 2

class TestMathOperations:
    """测试类"""
    
    def test_multiplication(self):
        assert 2 * 3 == 6
    
    def test_division(self):
        assert 6 / 2 == 3

2. 断言语法

python 复制代码
def test_assertions():
    # 基本断言
    assert value == expected
    assert value != unexpected
    
    # 异常断言
    with pytest.raises(ValueError):
        int("abc")
    
    # 警告断言
    with pytest.warns(UserWarning):
        warnings.warn("test", UserWarning)
    
    # 近似相等
    assert 0.1 + 0.2 == pytest.approx(0.3)

3. 参数化测试

python 复制代码
import pytest

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 3),
    (3, 4)
])
def test_increment(input, expected):
    assert input + 1 == expected

# 多参数组合
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
    assert x * y == x * y

二、配置文件 pytest.ini

pytest.ini是pytest的配置文件,用于设置pytest的执行参数。它可以放在项目根目录下,影响该目录及其子目录中的测试。

示例pytest.ini文件:

ini 复制代码
[pytest]
addopts = -v -s
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    fast: marks tests as fast

参数说明:

  • addopts:默认命令行参数,例如-v表示详细输出,-s表示输出打印信息。
  • testpaths:指定测试目录。
  • python_files:指定测试文件命名模式。
  • python_classes:指定测试类命名模式。
  • python_functions:指定测试函数命名模式。
  • markers:自定义标记,用于分组测试。

1. 基本配置

ini 复制代码
[pytest]
# 默认命令行选项
addopts = 
    -v           # 详细输出
    --tb=short   # 简短的traceback
    --strict-markers
    --disable-warnings
    --html=reports/report.html
    --self-contained-html

# 文件匹配模式
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*

# 测试目录
testpaths = 
    tests
    integration_tests
    e2e_tests

# 标记定义
markers =
    smoke: 冒烟测试
    regression: 回归测试
    slow: 慢速测试
    api: API接口测试
    db: 数据库测试

# 忽略目录
norecursedirs = 
    .git
    .venv
    node_modules
    __pycache__
    
# 日志配置
log_format = %(asctime)s [%(levelname)s] %(message)s
log_date_format = %Y-%m-%d %H:%M:%S
log_cli = true
log_cli_level = INFO

# 自定义配置
minversion = 6.0
xfail_strict = true

2. 环境特定配置

ini 复制代码
# pytest.ini.dev (开发环境)
[pytest]
addopts = -v -x --lf --tb=short

# pytest.ini.ci (持续集成环境)
[pytest]
addopts = -v --junitxml=junit.xml --tb=line

三、pytest.main() 函数详解

pytest.main()是运行测试的入口函数,可以在Python脚本中调用以运行测试。它接受一个参数列表,类似于命令行参数。

示例run_test.py

python 复制代码
import pytest

if __name__ == "__main__":
    # 运行所有测试
    pytest.main(["-v", "tests/"])
    
    # 运行特定标记的测试
    # pytest.main(["-m", "slow"])
    
    # 运行特定文件
    # pytest.main(["tests/test_example.py"])

1. 基本用法

python 复制代码
# run_tests.py
import pytest

if __name__ == "__main__":
    # 方式1: 传递参数列表
    pytest.main(["-v", "tests/"])
    
    # 方式2: 使用字符串
    pytest.main("-v tests/")
    
    # 方式3: 退出代码处理
    exit_code = pytest.main(["-x", "tests/test_api.py"])
    if exit_code != 0:
        print("测试失败!")
        exit(exit_code)

2. 高级配置

python 复制代码
import sys
import pytest

def run_tests():
    """自定义测试运行器"""
    
    # 动态添加路径
    sys.path.insert(0, "src")
    
    # 运行选项
    args = [
        "-v",                    # 详细模式
        "--tb=short",            # 简短回溯
        "--maxfail=3",           # 最多失败数
        "--junitxml=results.xml",# JUnit报告
        "--html=report.html",    # HTML报告
        "--self-contained-html",
        "--cov=src",             # 覆盖率
        "--cov-report=html",
        "-m", "not slow",        # 排除慢测试
        "tests/",
    ]
    
    # 运行测试
    return pytest.main(args)

if __name__ == "__main__":
    result = run_tests()
    sys.exit(result)

3. 插件集成

python 复制代码
import pytest

def run_with_plugins():
    """使用插件运行"""
    plugins = [
        "pytest-html",
        "pytest-cov",
        "pytest-xdist",
        "pytest-timeout"
    ]
    
    return pytest.main([
        "-v",
        "-n", "auto",  # 多进程
        "--timeout=30",
        "tests/"
    ])

四、YAML 用例配置

在测试中,我们有时会使用YAML文件来管理测试数据。YAML格式易于读写,可以存储复杂的数据结构。

示例test_data.yml

yaml 复制代码
- name: "test login success"
  url: "/login"
  method: "POST"
  data:
    username: "admin"
    password: "123456"
  expected:
    code: 200
    message: "login success"

- name: "test login failure"
  url: "/login"
  method: "POST"
  data:
    username: "admin"
    password: "wrong"
  expected:
    code: 401
    message: "login failed"

在测试中读取YAML文件:

python 复制代码
import yaml
import pytest

def load_test_data(file_path):
    with open(file_path, 'r') as f:
        return yaml.safe_load(f)

@pytest.mark.parametrize("test_case", load_test_data("test_data.yml"))
def test_login(test_case):
    # 使用test_case中的数据进行测试
    pass

1. 测试数据配置

yaml 复制代码
# test_data/api_tests.yml
api_tests:
  login:
    name: "用户登录接口测试"
    base_url: "https://api.example.com"
    endpoint: "/api/v1/login"
    method: "POST"
    headers:
      Content-Type: "application/json"
    test_cases:
      - case_id: "TC_LOGIN_001"
        name: "正常登录"
        data:
          username: "testuser"
          password: "password123"
        expected:
          status_code: 200
          response:
            code: 0
            message: "登录成功"
            data:
              token: !!str  # 类型验证
        
      - case_id: "TC_LOGIN_002"
        name: "密码错误"
        data:
          username: "testuser"
          password: "wrong"
        expected:
          status_code: 200
          response:
            code: 1001
            message: "密码错误"

2. YAML 加载器

python 复制代码
# utils/yaml_loader.py
import yaml
import os
from typing import Dict, Any

class YamlTestLoader:
    
    @staticmethod
    def load_test_cases(file_path: str) -> Dict[str, Any]:
        """加载YAML测试用例"""
        with open(file_path, 'r', encoding='utf-8') as f:
            return yaml.safe_load(f)
    
    @staticmethod
    def load_multiple_files(directory: str) -> Dict:
        """加载目录下所有YAML文件"""
        test_cases = {}
        for file in os.listdir(directory):
            if file.endswith(('.yml', '.yaml')):
                file_path = os.path.join(directory, file)
                test_name = os.path.splitext(file)[0]
                test_cases[test_name] = YamlTestLoader.load_test_cases(file_path)
        return test_cases

# 使用示例
test_data = YamlTestLoader.load_test_cases("test_data/api_tests.yml")

3. 参数化使用

python 复制代码
import pytest
from utils.yaml_loader import YamlTestLoader

# 加载测试数据
test_cases = YamlTestLoader.load_test_cases("test_data/api_tests.yml")

class TestAPI:
    
    @pytest.mark.parametrize("test_case", test_cases["api_tests"]["login"]["test_cases"])
    def test_login(self, test_case):
        """使用YAML数据驱动的测试"""
        # 准备请求
        request_data = test_case["data"]
        
        # 发送请求
        response = self.make_request(
            method="POST",
            url=test_case.get("base_url", "") + test_case["endpoint"],
            data=request_data
        )
        
        # 验证响应
        assert response.status_code == test_case["expected"]["status_code"]
        assert response.json()["code"] == test_case["expected"]["response"]["code"]

五、请求和响应校验

在接口测试中,我们经常使用requests库发送HTTP请求,并对响应进行校验。

python 复制代码
import requests

def test_request():
    url = "http://httpbin.org/get"
    response = requests.get(url)
    
    # 校验状态码
    assert response.status_code == 200
    
    # 校验响应头
    assert response.headers["Content-Type"] == "application/json"
    
    # 校验响应体
    json_data = response.json()
    assert json_data["url"] == url

1. Request 封装

python 复制代码
# core/request_client.py
import requests
import json
from typing import Dict, Any, Optional

class RequestClient:
    
    def __init__(self, base_url: str = ""):
        self.base_url = base_url
        self.session = requests.Session()
        self.default_headers = {
            "Content-Type": "application/json",
            "User-Agent": "Pytest-API-Test"
        }
    
    def request(
        self,
        method: str,
        endpoint: str,
        params: Optional[Dict] = None,
        data: Optional[Dict] = None,
        json_data: Optional[Dict] = None,
        headers: Optional[Dict] = None,
        timeout: int = 30
    ) -> requests.Response:
        """发送请求"""
        
        # 合并headers
        final_headers = {**self.default_headers, **(headers or {})}
        
        # 构建URL
        url = f"{self.base_url}{endpoint}"
        
        # 发送请求
        response = self.session.request(
            method=method.upper(),
            url=url,
            params=params,
            data=data,
            json=json_data,
            headers=final_headers,
            timeout=timeout
        )
        
        return response
    
    # 快捷方法
    def get(self, endpoint: str, **kwargs):
        return self.request("GET", endpoint, **kwargs)
    
    def post(self, endpoint: str, **kwargs):
        return self.request("POST", endpoint, **kwargs)
    
    def put(self, endpoint: str, **kwargs):
        return self.request("PUT", endpoint, **kwargs)
    
    def delete(self, endpoint: str, **kwargs):
        return self.request("DELETE", endpoint, **kwargs)

2. Response 校验器

python 复制代码
# core/response_validator.py
import json
import jsonschema
from typing import Dict, Any, List
from pydantic import BaseModel, ValidationError

class ResponseValidator:
    
    @staticmethod
    def validate_status_code(response, expected_code: int):
        """验证状态码"""
        assert response.status_code == expected_code, \
            f"期望状态码 {expected_code}, 实际 {response.status_code}"
    
    @staticmethod
    def validate_json_schema(response, schema: Dict):
        """验证JSON Schema"""
        try:
            jsonschema.validate(
                instance=response.json(),
                schema=schema
            )
        except jsonschema.ValidationError as e:
            raise AssertionError(f"Schema验证失败: {e.message}")
    
    @staticmethod
    def validate_pydantic_model(response, model_class: BaseModel):
        """使用Pydantic模型验证"""
        try:
            model_class(**response.json())
        except ValidationError as e:
            raise AssertionError(f"模型验证失败: {e}")
    
    @staticmethod
    def validate_response_time(response, max_time: float):
        """验证响应时间"""
        assert response.elapsed.total_seconds() <= max_time, \
            f"响应时间 {response.elapsed.total_seconds()}s 超过限制 {max_time}s"
    
    @staticmethod
    def validate_headers(response, expected_headers: Dict):
        """验证响应头"""
        for key, value in expected_headers.items():
            assert key in response.headers, f"缺少响应头: {key}"
            assert response.headers[key] == value, \
                f"响应头 {key} 不匹配: 期望 {value}, 实际 {response.headers[key]}"

class ComprehensiveValidator:
    """综合校验器"""
    
    def __init__(self):
        self.validations = []
    
    def add_validation(self, validation_func, *args, **kwargs):
        """添加校验规则"""
        self.validations.append((validation_func, args, kwargs))
        return self
    
    def run_all(self, response):
        """运行所有校验"""
        errors = []
        for func, args, kwargs in self.validations:
            try:
                func(response, *args, **kwargs)
            except AssertionError as e:
                errors.append(str(e))
        
        if errors:
            raise AssertionError("\n".join(errors))

3. 测试中使用

python 复制代码
import pytest
from core.request_client import RequestClient
from core.response_validator import ResponseValidator, ComprehensiveValidator

class TestAPIIntegration:
    
    @pytest.fixture
    def client(self):
        return RequestClient(base_url="https://api.example.com")
    
    def test_user_api(self, client):
        """完整API测试示例"""
        
        # 1. 发送请求
        response = client.post("/api/users", json_data={
            "name": "John",
            "email": "john@example.com"
        })
        
        # 2. 使用综合校验器
        validator = ComprehensiveValidator()
        validator.add_validation(
            ResponseValidator.validate_status_code,
            expected_code=201
        )
        validator.add_validation(
            ResponseValidator.validate_json_schema,
            schema={
                "type": "object",
                "properties": {
                    "id": {"type": "integer"},
                    "name": {"type": "string"},
                    "email": {"type": "string", "format": "email"}
                },
                "required": ["id", "name", "email"]
            }
        )
        validator.add_validation(
            ResponseValidator.validate_response_time,
            max_time=2.0
        )
        
        validator.run_all(response)
        
        # 3. 业务逻辑验证
        response_data = response.json()
        assert response_data["name"] == "John"
        assert "@" in response_data["email"]

六、conftest.py 作用详解

conftest.py是pytest的本地插件,用于定义钩子函数和夹具(fixture)。该文件可以放在项目根目录或任何测试子目录中,作用范围为其所在目录及其子目录。

常用功能:

  • 定义夹具(fixture)供多个测试文件使用。
  • 定义钩子函数,例如修改测试收集过程。

示例conftest.py

python 复制代码
import pytest

@pytest.fixture
def login():
    return "token"

@pytest.fixture
def setup_database():
    # 设置数据库
    yield
    # 清理数据库

# 钩子函数示例
def pytest_runtest_setup(item):
    # 在每个测试运行前执行
    pass

1. conftest.py 结构

python 复制代码
# conftest.py
import pytest
import os
from typing import Dict, Generator
from core.request_client import RequestClient

# 配置hooks
def pytest_configure(config):
    """在测试运行前配置"""
    config.addinivalue_line(
        "markers", "integration: 集成测试"
    )

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

# Fixtures定义
@pytest.fixture(scope="session")
def config() -> Dict:
    """读取配置"""
    import yaml
    with open("config/test_config.yaml") as f:
        return yaml.safe_load(f)

@pytest.fixture(scope="session")
def api_client(config) -> Generator:
    """API客户端"""
    client = RequestClient(base_url=config["base_url"])
    yield client
    client.session.close()

@pytest.fixture
def auth_client(api_client, config):
    """认证客户端"""
    # 登录获取token
    response = api_client.post("/login", json_data={
        "username": config["test_user"],
        "password": config["test_password"]
    })
    token = response.json()["token"]
    
    # 设置认证头
    api_client.default_headers["Authorization"] = f"Bearer {token}"
    return api_client

@pytest.fixture
def db_connection():
    """数据库连接"""
    import psycopg2
    conn = psycopg2.connect(
        host="localhost",
        database="testdb",
        user="test"
    )
    yield conn
    conn.close()

# 自定义fixture
@pytest.fixture
def create_test_user(db_connection):
    """创建测试用户"""
    def _create(username: str, email: str):
        cursor = db_connection.cursor()
        cursor.execute(
            "INSERT INTO users (username, email) VALUES (%s, %s) RETURNING id",
            (username, email)
        )
        user_id = cursor.fetchone()[0]
        db_connection.commit()
        return user_id
    
    yield _create
    
    # 清理
    cursor = db_connection.cursor()
    cursor.execute("DELETE FROM users WHERE username LIKE 'test_%'")
    db_connection.commit()

# 自定义钩子
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """获取测试结果"""
    outcome = yield
    rep = outcome.get_result()
    
    if rep.when == "call" and rep.failed:
        # 失败时截图(UI测试)
        if hasattr(item, "funcargs") and "driver" in item.funcargs:
            driver = item.funcargs["driver"]
            screenshot_path = f"screenshots/{item.name}.png"
            driver.save_screenshot(screenshot_path)

2. conftest.py 层级结构

text 复制代码
project/
├── conftest.py                    # 全局fixtures
├── tests/
│   ├── conftest.py                # 测试目录级fixtures
│   ├── api/
│   │   ├── conftest.py            # API测试专用fixtures
│   │   └── test_user_api.py
│   └── ui/
│       ├── conftest.py            # UI测试专用fixtures
│       └── test_login.py
└── config/
    └── test_config.yaml

七、Pytest 架构图 (Mermaid)

测试执行流程
测试组织
Fixture 系统
插件系统
Pytest 核心框架
pytest.ini 配置
pytest.main 入口
测试发现
测试执行引擎
结果报告生成
pytest-html
pytest-cov
pytest-xdist
自定义插件
conftest.py
Fixture 注册表
测试依赖注入
测试文件 .py
测试数据 .yaml/.json
测试标记 markers
参数化 parametrize
测试用例实例
Request/Response
断言校验
测试结果

八、Pytest 工作流程图

Plugin Manager Reporter Test Executor Fixture Manager Test Discoverer Config Loader Test Runner Plugin Manager Reporter Test Executor Fixture Manager Test Discoverer Config Loader Test Runner 测试执行流程 alt [测试通过] [测试失败] [测试跳过] loop [每个测试用例] 加载 pytest.ini 配置 返回配置参数 发现测试用例 扫描 test_*.py 文件 收集测试函数/类 返回测试用例列表 准备fixtures 解析依赖关系 执行fixture函数 返回fixture值 执行测试用例 触发插件钩子 (pytest_runtest_call) 运行测试代码 执行断言 测试通过 测试失败 + 异常信息 测试跳过 记录测试结果 清理fixtures 生成测试报告 创建HTML/XML报告 统计测试结果 触发结束钩子 (pytest_sessionfinish) 插件清理工作

九、最佳实践总结

1. 项目结构

text 复制代码
project/
├── pytest.ini
├── conftest.py
├── requirements.txt
├── src/                    # 源代码
├── tests/                  # 测试代码
│   ├── conftest.py
│   ├── unit/              # 单元测试
│   ├── integration/       # 集成测试
│   ├── api/              # API测试
│   │   ├── conftest.py
│   │   └── test_*.py
│   └── fixtures/          # 测试数据
│       └── *.yaml
├── reports/               # 测试报告
├── config/               # 配置文件
└── run_tests.py          # 测试启动脚本

2. 常用命令

bash 复制代码
# 基本运行
pytest tests/ -v
pytest tests/test_api.py::TestLogin

# 标记运行
pytest -m "smoke and not slow"
pytest -m "api or integration"

# 并行运行
pytest -n auto

# 失败重试
pytest --reruns 3 --reruns-delay 2

# 覆盖率
pytest --cov=src --cov-report=html

# 调试模式
pytest --pdb

3. 实用技巧

python 复制代码
# 1. 跳过测试
@pytest.mark.skip(reason="功能未实现")
def test_unimplemented():
    pass

# 2. 条件跳过
@pytest.mark.skipif(
    sys.platform != "linux",
    reason="仅在Linux运行"
)

# 3. 预期失败
@pytest.mark.xfail(reason="已知bug")
def test_buggy_feature():
    assert False

# 4. 临时目录
def test_with_tempdir(tmpdir):
    temp_file = tmpdir.join("test.txt")
    temp_file.write("content")
    assert temp_file.read() == "content"

# 5. 测试日志
def test_with_logging(caplog):
    logging.getLogger().info("测试日志")
    assert "测试日志" in caplog.text
相关推荐
0思必得02 小时前
[Web自动化] JavaScriptAJAX与Fetch API
运维·前端·javascript·python·自动化·html·web自动化
心态特好2 小时前
pytorch和tenserflow详解
人工智能·pytorch·python
爱上妖精的尾巴2 小时前
7-1 WPS JS宏 Object对象创建的几种方法
开发语言·前端·javascript
ZePingPingZe2 小时前
静态代理、JDK和Cglib动态代理、回调
java·开发语言
2501_921649492 小时前
iTick 全球外汇、股票、期货、基金实时行情 API 接口文档详解
开发语言·python·websocket·金融·restful
万粉变现经纪人2 小时前
如何解决 pip install 代理报错 SOCKS5 握手失败 ReadTimeoutError 问题
java·python·pycharm·beautifulsoup·bug·pandas·pip
你怎么知道我是队长2 小时前
python---进程
开发语言·chrome·python
C++ 老炮儿的技术栈2 小时前
时序数据库 相对于关系型数据库,有什么区别
c语言·开发语言·c++·机器人·时序数据库·visual studio
风月歌2 小时前
2025-2026计算机毕业设计选题指导,java|springboot|ssm项目成品推荐
java·python·小程序·毕业设计·php·源码