PyTest 介绍和使用指南

什么是 PyTest?

PyTest 是一个功能强大的 Python 测试框架,具有以下特点:

  • 简单易用:只需要使用 assert 语句即可编写测试
  • 功能丰富:支持参数化测试、fixture、插件系统等高级功能
  • 自动发现:自动发现并运行测试文件
  • 详细报告:提供清晰的测试结果和失败信息

安装 PyTest

pip install pytest

基本用法

1. 简单的测试示例

创建一个测试文件 test_sample.py

# test_sample.py

def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

运行测试:

pytest test_sample.py

2. 测试类和分组

# test_calculator.py

class Calculator:
    def multiply(self, a, b):
        return a * b
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b

class TestCalculator:
    def test_multiply(self):
        calc = Calculator()
        assert calc.multiply(3, 4) == 12
        assert calc.multiply(-2, 5) == -10
    
    def test_divide(self):
        calc = Calculator()
        assert calc.divide(10, 2) == 5
        assert calc.divide(5, 2) == 2.5
    
    def test_divide_by_zero(self):
        calc = Calculator()
        with pytest.raises(ValueError, match="Cannot divide by zero"):
            calc.divide(10, 0)

高级功能

1. Fixture(夹具)

Fixture 用于为测试提供预设环境和数据:

import pytest

@pytest.fixture
def sample_data():
    """提供测试数据"""
    return {"name": "Alice", "age": 30, "scores": [85, 92, 78]}

@pytest.fixture
def database_connection():
    """模拟数据库连接"""
    connection = "mock_database_connection"
    yield connection  # 测试执行前设置,yield 后清理
    # 清理代码
    connection = None

def test_data_processing(sample_data, database_connection):
    assert sample_data["name"] == "Alice"
    assert len(sample_data["scores"]) == 3
    assert database_connection is not None

2. 参数化测试

import pytest

@pytest.mark.parametrize("input_a, input_b, expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300)
])
def test_addition(input_a, input_b, expected):
    assert input_a + input_b == expected

@pytest.mark.parametrize("username, password, expected_result", [
    ("admin", "secret", True),
    ("user", "wrong", False),
    ("", "", False)
])
def test_login(username, password, expected_result):
    # 模拟登录逻辑
    result = login_function(username, password)
    assert result == expected_result

3. 标记(Markers)

import pytest

@pytest.mark.slow
def test_expensive_operation():
    # 这个测试运行很慢
    result = expensive_computation()
    assert result is not None

@pytest.mark.skip(reason="功能尚未实现")
def test_unimplemented_feature():
    assert False

@pytest.mark.skipif(sys.version_info < (3, 7), reason="需要 Python 3.7 或更高版本")
def test_python37_feature():
    # 使用 Python 3.7 特性的测试
    pass

@pytest.mark.xfail
def test_experimental_feature():
    # 预期会失败的测试
    assert experimental_function() == "expected"

常用命令行选项

# 运行所有测试
pytest

# 运行特定文件
pytest test_module.py

# 运行特定目录
pytest tests/

# 运行包含特定字符串的测试
pytest -k "add"  # 运行所有包含 "add" 的测试

# 运行标记的测试
pytest -m slow   # 只运行标记为 @pytest.mark.slow 的测试

# 显示详细输出
pytest -v

# 在第一次失败时停止
pytest -x

# 生成测试报告
pytest --html=report.html

# 并行运行测试
pytest -n 4  # 使用 4 个进程

测试目录结构

project/
├── src/
│   └── mymodule.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py      # 共享的 fixture
│   ├── test_basic.py
│   ├── test_advanced.py
│   └── fixtures/
│       └── test_data.py
├── setup.py
└── pytest.ini          # pytest 配置文件

配置文件 pytest.ini

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    integration: integration tests

断言和异常测试

import pytest

def test_assertions():
    # 基本断言
    assert 1 + 1 == 2
    
    # 包含性检查
    assert "hello" in "hello world"
    assert 1 in [1, 2, 3]
    
    # 类型检查
    assert isinstance("hello", str)
    
    # 近似相等
    assert 0.1 + 0.2 == pytest.approx(0.3)

def test_exceptions():
    # 测试异常
    with pytest.raises(ValueError):
        int("not_a_number")
    
    # 测试异常消息
    with pytest.raises(ValueError, match="invalid literal"):
        int("not_a_number")

实用技巧

1. 临时目录

def test_temp_files(tmp_path):
    # tmp_path 是 pytest 提供的临时目录 fixture
    test_file = tmp_path / "test.txt"
    test_file.write_text("Hello, pytest!")
    
    assert test_file.read_text() == "Hello, pytest!"
    assert test_file.exists()

2. Monkeypatch

def test_monkeypatch(monkeypatch):
    # 临时修改函数或属性
    import os
    
    def mock_getcwd():
        return "/mock/directory"
    
    monkeypatch.setattr(os, "getcwd", mock_getcwd)
    assert os.getcwd() == "/mock/directory"

3. 捕获输出

def test_output(capsys):
    print("Hello, World!")
    captured = capsys.readouterr()
    assert captured.out == "Hello, World!\n"

总结

PyTest 是一个强大而灵活的测试框架,通过简单的语法和丰富的功能,让编写和维护测试变得更加容易。掌握 PyTest 可以显著提高代码质量和开发效率。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐