00.pytest入门

Pytest 入门完全指南

1. 什么是 Pytest

1.1 Pytest 简介

Pytest 是 Python 中最流行的测试框架之一,它简单易用、功能强大,被广泛应用于单元测试、功能测试、集成测试和接口测试等场景。

Pytest 的核心特点

  • 简洁:使用简单的 assert 语句进行断言,无需学习复杂的断言方法
  • 易上手:测试函数只需以 test_ 开头即可被发现和执行
  • 功能强大:支持参数化、Fixture、插件等高级特性
  • 信息丰富:测试失败时提供详细的错误信息
  • 插件丰富:拥有大量第三方插件,可扩展性强

1.2 Pytest vs Unittest

Python 内置了 unittest 测试框架,但 pytest 相比 unittest 有很多优势:

对比项 Pytest Unittest
断言方式 使用简单的 assert 需要使用 self.assertEqual() 等方法
测试发现 自动发现 test_ 开头的函数 需要继承 TestCase
代码量 更少、更简洁 较多、需要更多样板代码
Fixture 强大灵活的 fixture 系统 只有 setUptearDown
参数化 内置参数化支持 需要额外库支持
插件生态 丰富的插件生态 插件较少
兼容性 可以运行 unittest 测试 不能运行 pytest 测试

Unittest 示例(繁琐):

import unittest

class TestMath(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(1 + 1, 2)

    def test_subtraction(self):
        self.assertEqual(5 - 3, 2)

if __name__ == '__main__':
    unittest.main()

Pytest 示例(简洁):

def test_addition():
    assert 1 + 1 == 2

def test_subtraction():
    assert 5 - 3 == 2

1.3 Pytest 的应用场景

Pytest 可以用于以下测试场景:

  1. 单元测试:测试单个函数或方法的功能
  2. 集成测试:测试多个模块之间的协作
  3. 接口测试(API 测试):测试 Web API 的功能和性能
  4. UI 自动化测试:配合 Selenium、Playwright 等工具进行 UI 测试
  5. 功能测试:测试软件的完整功能
  6. 回归测试:确保修改后的代码没有破坏现有功能

2. 安装 Pytest

2.1 安装前准备

在安装 pytest 之前,请确保你已经安装了 Python。可以在命令行中运行以下命令检查:

# 检查 Python 版本
python --version

# 或者
python3 --version

输出示例

Python 3.9.0

注意:Pytest 支持 Python 3.7 及以上版本。

2.2 使用 pip 安装

pip 是 Python 的包管理工具,使用它可以轻松安装 pytest。

安装命令

# Windows
pip install pytest

# Mac/Linux
pip3 install pytest

# 如果遇到权限问题,可以使用
pip install pytest --user

安装过程输出示例

Collecting pytest
  Downloading pytest-7.4.0-py3-none-any.whl (323 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 323.3/323.3 kB 5.0 MB/s eta 0:00:00
Installing collected packages: pytest
Successfully installed pytest-7.4.0

2.3 验证安装

安装完成后,验证 pytest 是否安装成功:

# 查看 pytest 版本
pytest --version

输出示例

pytest 7.4.0

如果显示版本号,说明安装成功!

2.4 在虚拟环境中安装(推荐)

为什么使用虚拟环境?

  • 隔离项目依赖,避免不同项目之间的依赖冲突
  • 保持系统 Python 环境干净
  • 便于管理项目依赖

创建和使用虚拟环境

# 第一步:创建虚拟环境
python -m venv venv

# 第二步:激活虚拟环境
# Windows
venvScriptsactivate

# Mac/Linux
source venv/bin/activate

# 第三步:在虚拟环境中安装 pytest
pip install pytest

# 第四步:验证安装
pytest --version

激活虚拟环境后的命令行提示符

(venv) C:UsersAdministratorDesktopproject>

注意命令行前面的 (venv) 表示虚拟环境已激活。

2.5 安装常用的 pytest 插件

Pytest 有很多有用的插件,以下是一些常用的:

# 生成 HTML 测试报告
pip install pytest-html

# 分布式执行测试(多进程并行)
pip install pytest-xdist

# 测试排序
pip install pytest-ordering

# 失败重试
pip install pytest-rerunfailures

# 生成 Allure 报告
pip install allure-pytest

# 一次性安装多个插件
pip install pytest pytest-html pytest-xdist pytest-rerunfailures

2.6 使用 requirements.txt 管理依赖

在项目中,推荐使用 requirements.txt 文件管理依赖:

创建 requirements.txt

pytest==7.4.0
pytest-html==4.0.0
pytest-xdist==3.3.0
requests==2.31.0

安装依赖

pip install -r requirements.txt

3. 编写第一个测试用例

3.1 创建测试文件

Pytest 会自动发现以 test_ 开头或以 _test 结尾的测试文件。

创建文件test_first.py

# test_first.py
# 这是你的第一个 pytest 测试文件

def test_hello():
    """测试:Hello World"""
    assert True  # 断言为真,测试通过

def test_addition():
    """测试:加法运算"""
    result = 1 + 1
    assert result == 2  # 断言 1 + 1 等于 2

def test_string():
    """测试:字符串操作"""
    message = "Hello, Pytest!"
    assert "Pytest" in message  # 断言 message 中包含 "Pytest"

3.2 运行测试

打开命令行,进入测试文件所在目录,运行:

pytest test_first.py

输出示例

======================== test session starts ========================
platform win32 -- Python 3.9.0, pytest-7.4.0, pluggy-1.0.0
rootdir: C:UsersAdministratorDesktoppython自动化测试
collected 3 items

test_first.py ...                                              [100%]

======================== 3 passed in 0.02s ========================

输出解释

  • platform win32:运行平台(Windows)
  • Python 3.9.0, pytest-7.4.0:Python 和 pytest 版本
  • collected 3 items:发现了 3 个测试用例
  • test_first.py ...:3 个点表示 3 个测试都通过了
  • 3 passed in 0.02s:3 个测试通过,耗时 0.02 秒

3.3 查看详细输出

使用 -v 参数查看更详细的输出:

pytest -v test_first.py

输出示例

======================== test session starts ========================
platform win32 -- Python 3.9.0, pytest-7.4.0, pluggy-1.0.0
rootdir: C:UsersAdministratorDesktoppython自动化测试
collected 3 items

test_first.py::test_hello PASSED                               [ 33%]
test_first.py::test_addition PASSED                            [ 66%]
test_first.py::test_string PASSED                              [100%]

======================== 3 passed in 0.02s ========================

现在可以看到每个测试用例的名称和状态!

3.4 测试状态符号

在 pytest 的输出中,每个测试用例的状态用一个字符表示:

符号 含义 说明
. PASSED 测试通过
F FAILED 测试失败
E ERROR 测试出错(代码有异常)
s SKIPPED 测试被跳过
x XFAIL 预期失败的测试确实失败了
X XPASS 预期失败的测试却通过了

示例

test_example.py ..F.s                                          [100%]

表示:2 个通过,1 个失败,1 个通过,1 个跳过


4. 理解断言(Assert)

4.1 什么是断言

断言(Assert) 是测试中用来验证结果是否符合预期的语句。如果断言成功,测试通过;如果断言失败,测试失败。

在 pytest 中,使用 Python 内置的 assert 语句进行断言,这比其他测试框架的断言方法更加简洁直观。

4.2 基本断言用法

# test_assert.py

def test_equal():
    """测试:相等"""
    assert 1 + 1 == 2

def test_not_equal():
    """测试:不相等"""
    assert 1 + 1 != 3

def test_greater():
    """测试:大于"""
    assert 5 > 3

def test_less():
    """测试:小于"""
    assert 3 < 5

def test_true():
    """测试:为真"""
    assert True

def test_false():
    """测试:为假"""
    assert not False

def test_in():
    """测试:包含"""
    assert "hello" in "hello world"

def test_not_in():
    """测试:不包含"""
    assert "goodbye" not in "hello world"

def test_is_none():
    """测试:为 None"""
    result = None
    assert result is None

def test_is_not_none():
    """测试:不为 None"""
    result = "value"
    assert result is not None

4.3 常见断言类型

4.3.1 相等断言

def test_equality():
    # 数字相等
    assert 1 + 1 == 2

    # 字符串相等
    assert "hello" == "hello"

    # 列表相等
    assert [1, 2, 3] == [1, 2, 3]

    # 字典相等
    assert {"name": "张三"} == {"name": "张三"}

4.3.2 布尔断言

def test_boolean():
    # 断言为真
    assert True
    assert 1  # 非零数字为真
    assert "hello"  # 非空字符串为真
    assert [1, 2]  # 非空列表为真

    # 断言为假(使用 not)
    assert not False
    assert not 0  # 0 为假
    assert not ""  # 空字符串为假
    assert not []  # 空列表为假

4.3.3 包含断言

def test_contains():
    # 字符串包含
    assert "pytest" in "learning pytest is fun"

    # 列表包含
    assert 2 in [1, 2, 3]

    # 字典包含键
    assert "name" in {"name": "张三", "age": 25}

4.3.4 类型断言

def test_type():
    # 检查类型
    assert isinstance(1, int)
    assert isinstance("hello", str)
    assert isinstance([1, 2, 3], list)
    assert isinstance({"key": "value"}, dict)

    # 检查不是某类型
    assert not isinstance("hello", int)

4.3.5 比较断言

def test_comparison():
    # 大于
    assert 10 > 5

    # 大于等于
    assert 10 >= 10

    # 小于
    assert 5 < 10

    # 小于等于
    assert 5 <= 5

    # 近似相等(浮点数比较)
    import math
    assert math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9)

4.4 断言失败时的输出

当断言失败时,pytest 会显示详细的错误信息,帮助你快速定位问题。

测试文件

# test_fail.py

def test_fail_example():
    """这个测试会失败"""
    result = 1 + 1
    assert result == 3, "1 + 1 应该等于 2,不是 3"

运行测试

pytest -v test_fail.py

输出示例

======================== test session starts ========================
platform win32 -- Python 3.9.0, pytest-7.4.0
collected 1 item

test_fail.py::test_fail_example FAILED                          [100%]

======================== FAILURES ========================
________________________ test_fail_example ________________________

    def test_fail_example():
        """这个测试会失败"""
        result = 1 + 1
>       assert result == 3, "1 + 1 应该等于 2,不是 3"
E       AssertionError: 1 + 1 应该等于 2,不是 3
E       assert 2 == 3

test_fail.py:5: AssertionError
======================== 1 failed in 0.05s ========================

输出解释

  • > 表示断言失败的代码行
  • E 表示错误信息
  • assert 2 == 3 显示了实际值(2)和期望值(3)
  • 自定义消息 “1 + 1 应该等于 2,不是 3” 也会显示

4.5 添加断言消息

可以在断言后添加自定义消息,当断言失败时会显示:

def test_with_message():
    age = 17
    assert age >= 18, f"年龄必须大于等于 18,当前年龄是 {age}"

断言失败时的输出

E       AssertionError: 年龄必须大于等于 18,当前年龄是 17
E       assert 17 >= 18

4.6 复杂断言示例

def test_complex_assertions():
    # 用户数据
    user = {
        "name": "张三",
        "age": 25,
        "email": "zhangsan@example.com",
        "roles": ["user", "admin"]
    }

    # 断言用户名正确
    assert user["name"] == "张三", "用户名不匹配"

    # 断言年龄在有效范围内
    assert 0 < user["age"] < 120, "年龄不在有效范围内"

    # 断言邮箱格式正确(简单检查)
    assert "@" in user["email"], "邮箱格式不正确"

    # 断言用户有 admin 角色
    assert "admin" in user["roles"], "用户没有 admin 角色"

    # 断言角色列表长度
    assert len(user["roles"]) == 2, "角色数量不正确"

5. 测试函数的命名规则

5.1 文件命名规则

Pytest 会自动发现符合以下命名规则的测试文件:

命名规则 示例 是否会被发现
test_*.py test_login.py ✅ 是
*_test.py login_test.py ✅ 是
*.py(其他) login.py ❌ 否

推荐使用 test_ 前缀

project/
├── test_login.py      ✅ 推荐
├── test_user.py       ✅ 推荐
├── login_test.py      ✅ 可以
└── login.py           ❌ 不会被发现

5.2 函数命名规则

测试函数必须以 test_ 开头:

# test_example.py

# ✅ 正确:以 test_ 开头
def test_login():
    assert True

def test_user_creation():
    assert True

def test_api_response():
    assert True

# ❌ 错误:不以 test_ 开头(不会被发现)
def login():
    assert True

def check_user():
    assert True

5.3 类命名规则

测试类必须以 Test 开头,且不能有 __init__ 方法:

# test_class.py

# ✅ 正确:类名以 Test 开头
class TestLogin:
    def test_user_login(self):
        assert True

    def test_admin_login(self):
        assert True

# ❌ 错误:类名不以 Test 开头
class LoginTest:
    def test_login(self):
        assert True  # 不会被发现

# ❌ 错误:有 __init__ 方法
class TestUser:
    def __init__(self):
        self.name = "test"

    def test_user(self):
        assert True  # 不会被发现

5.4 命名最佳实践

好的命名方式

# 描述性命名,清晰表达测试内容
def test_user_login_with_valid_credentials():
    """测试用户使用有效凭据登录"""
    pass

def test_user_login_with_invalid_password():
    """测试用户使用无效密码登录"""
    pass

def test_user_registration_with_existing_email():
    """测试用户使用已存在的邮箱注册"""
    pass

不好的命名方式

# ❌ 不够描述性
def test_1():
    pass

def test_login1():
    pass

def test_a():
    pass

6. 运行测试的多种方式

6.1 运行所有测试

# 运行当前目录及子目录下的所有测试
pytest

# 详细模式
pytest -v

6.2 运行指定文件

# 运行单个文件
pytest test_login.py

# 运行多个文件
pytest test_login.py test_user.py

6.3 运行指定目录

# 运行 tests 目录下的所有测试
pytest tests/

# 运行多个目录
pytest tests/ api_tests/

6.4 运行指定的测试函数

# 运行指定文件中的指定函数
pytest test_login.py::test_user_login

# 使用 -k 参数按名称筛选
pytest -k "login"  # 运行所有名称包含 "login" 的测试

6.5 运行指定的测试类

# 运行指定文件中的指定类
pytest test_user.py::TestUser

# 运行类中的指定方法
pytest test_user.py::TestUser::test_create_user

6.6 使用表达式筛选测试

# 运行名称包含 "login" 的测试
pytest -k "login"

# 运行名称包含 "login" 且包含 "user" 的测试
pytest -k "login and user"

# 运行名称包含 "login" 或 "register" 的测试
pytest -k "login or register"

# 运行名称包含 "login" 但不包含 "admin" 的测试
pytest -k "login and not admin"

6.7 常用命令行参数

参数 作用 示例
-v 详细输出 pytest -v
-s 显示 print 输出 pytest -s
-x 遇到失败立即停止 pytest -x
-q 简洁输出 pytest -q
--tb=short 简短的错误信息 pytest --tb=short
--collect-only 只收集测试,不运行 pytest --collect-only

6.8 组合使用参数

# 详细输出 + 显示 print + 遇到失败停止
pytest -xvs

# 详细输出 + 简短错误信息
pytest -v --tb=short

# 只运行包含 "login" 的测试,详细输出
pytest -k "login" -v

7. 项目结构组织

7.1 基本项目结构

my_project/
├── src/                    # 源代码目录
│   ├── __init__.py
│   ├── calculator.py
│   └── user.py
├── tests/                  # 测试目录
│   ├── __init__.py
│   ├── test_calculator.py
│   └── test_user.py
├── pytest.ini              # pytest 配置文件
├── requirements.txt        # 项目依赖
└── README.md

7.2 源代码示例

src/calculator.py

# src/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

7.3 测试代码示例

tests/test_calculator.py

# tests/test_calculator.py
import pytest
import sys
sys.path.insert(0, '../src')  # 添加源代码路径

from calculator import Calculator

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

    def setup_method(self):
        """每个测试方法执行前调用"""
        self.calc = Calculator()

    def test_add(self):
        """测试加法"""
        result = self.calc.add(1, 2)
        assert result == 3

    def test_add_negative(self):
        """测试负数加法"""
        result = self.calc.add(-1, -2)
        assert result == -3

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

    def test_multiply(self):
        """测试乘法"""
        result = self.calc.multiply(3, 4)
        assert result == 12

    def test_divide(self):
        """测试除法"""
        result = self.calc.divide(10, 2)
        assert result == 5

    def test_divide_by_zero(self):
        """测试除以零"""
        with pytest.raises(ValueError):
            self.calc.divide(10, 0)

7.4 pytest.ini 配置文件

pytest.ini

[pytest]
# 测试文件匹配模式
python_files = test_*.py

# 测试类匹配模式
python_classes = Test*

# 测试函数匹配模式
python_functions = test_*

# 测试目录
testpaths = tests

# 默认命令行参数
addopts = -v --tb=short

# 标记定义
markers =
    smoke: 冒烟测试
    integration: 集成测试
    slow: 慢速测试

7.5 运行项目测试

# 进入项目目录
cd my_project

# 运行所有测试
pytest

# 运行特定测试文件
pytest tests/test_calculator.py

# 运行详细模式
pytest -v

8. Setup 和 Teardown

8.1 什么是 Setup 和 Teardown

Setup(前置):在测试执行之前运行的代码,用于准备测试环境。

Teardown(后置):在测试执行之后运行的代码,用于清理测试环境。

8.2 函数级别的 Setup 和 Teardown

# test_setup_teardown.py

def setup_function():
    """每个测试函数执行前调用"""
    print("n--- 函数级别 Setup ---")

def teardown_function():
    """每个测试函数执行后调用"""
    print("n--- 函数级别 Teardown ---")

def test_example_1():
    print("执行测试 1")
    assert True

def test_example_2():
    print("执行测试 2")
    assert True

运行测试(使用 -s 查看 print 输出):

pytest -sv test_setup_teardown.py

输出示例

test_setup_teardown.py::test_example_1
--- 函数级别 Setup ---
执行测试 1
PASSED
--- 函数级别 Teardown ---

test_setup_teardown.py::test_example_2
--- 函数级别 Setup ---
执行测试 2
PASSED
--- 函数级别 Teardown ---

8.3 类级别的 Setup 和 Teardown

# test_class_setup.py

class TestExample:
    @classmethod
    def setup_class(cls):
        """整个类执行前调用一次"""
        print("n=== 类级别 Setup ===")
        cls.shared_data = "共享数据"

    @classmethod
    def teardown_class(cls):
        """整个类执行后调用一次"""
        print("n=== 类级别 Teardown ===")

    def setup_method(self):
        """每个测试方法执行前调用"""
        print("n--- 方法级别 Setup ---")

    def teardown_method(self):
        """每个测试方法执行后调用"""
        print("n--- 方法级别 Teardown ---")

    def test_method_1(self):
        print(f"执行测试 1,共享数据:{self.shared_data}")
        assert True

    def test_method_2(self):
        print(f"执行测试 2,共享数据:{self.shared_data}")
        assert True

输出示例

=== 类级别 Setup ===

--- 方法级别 Setup ---
执行测试 1,共享数据:共享数据
PASSED
--- 方法级别 Teardown ---

--- 方法级别 Setup ---
执行测试 2,共享数据:共享数据
PASSED
--- 方法级别 Teardown ---

=== 类级别 Teardown ===

8.4 模块级别的 Setup 和 Teardown

# test_module_setup.py

def setup_module():
    """整个模块(文件)执行前调用一次"""
    print("n>>> 模块级别 Setup <<<")

def teardown_module():
    """整个模块(文件)执行后调用一次"""
    print("n>>> 模块级别 Teardown <<<")

def test_1():
    print("执行测试 1")
    assert True

def test_2():
    print("执行测试 2")
    assert True

8.5 执行顺序

模块 Setup
    └── 类 Setup
            └── 方法 Setup
                    └── 测试执行
            └── 方法 Teardown
    └── 类 Teardown
模块 Teardown

8.6 实际应用场景

场景 1:数据库测试

class TestDatabase:
    @classmethod
    def setup_class(cls):
        """创建数据库连接"""
        cls.db = DatabaseConnection()
        cls.db.connect()

    @classmethod
    def teardown_class(cls):
        """关闭数据库连接"""
        cls.db.disconnect()

    def setup_method(self):
        """每个测试前开启事务"""
        self.db.begin_transaction()

    def teardown_method(self):
        """每个测试后回滚事务"""
        self.db.rollback()

    def test_insert_user(self):
        self.db.insert_user({"name": "张三"})
        assert self.db.get_user_count() == 1

场景 2:浏览器自动化测试

class TestBrowser:
    @classmethod
    def setup_class(cls):
        """启动浏览器"""
        cls.driver = webdriver.Chrome()

    @classmethod
    def teardown_class(cls):
        """关闭浏览器"""
        cls.driver.quit()

    def test_home_page(self):
        self.driver.get("https://example.com")
        assert "Example" in self.driver.title

9. 跳过测试和预期失败

9.1 跳过测试(skip)

使用 @pytest.mark.skip 装饰器跳过测试:

import pytest

@pytest.mark.skip(reason="功能尚未实现")
def test_not_implemented():
    """这个测试会被跳过"""
    assert False

@pytest.mark.skip
def test_skip_without_reason():
    """跳过但不提供原因"""
    assert False

运行结果

test_skip.py::test_not_implemented SKIPPED (功能尚未实现)
test_skip.py::test_skip_without_reason SKIPPED

9.2 条件跳过(skipif)

根据条件跳过测试:

import pytest
import sys

@pytest.mark.skipif(sys.version_info < (3, 9), reason="需要 Python 3.9+")
def test_python_39_feature():
    """只在 Python 3.9+ 上运行"""
    assert True

@pytest.mark.skipif(sys.platform == 'win32', reason="不支持 Windows")
def test_linux_only():
    """只在 Linux 上运行"""
    assert True

# 可以定义条件变量
is_database_available = False

@pytest.mark.skipif(not is_database_available, reason="数据库不可用")
def test_database():
    """需要数据库连接"""
    assert True

9.3 预期失败(xfail)

使用 @pytest.mark.xfail 标记预期会失败的测试:

import pytest

@pytest.mark.xfail(reason="已知 bug,等待修复")
def test_known_bug():
    """这个测试预期会失败"""
    assert 1 + 1 == 3

@pytest.mark.xfail(reason="功能待实现")
def test_todo():
    """待实现的功能"""
    raise NotImplementedError

运行结果

test_xfail.py::test_known_bug XFAIL (已知 bug,等待修复)
test_xfail.py::test_todo XFAIL (功能待实现)

9.4 跳过模块中的所有测试

import pytest

# 跳过整个模块
pytestmark = pytest.mark.skip(reason="整个模块跳过")

def test_1():
    assert True

def test_2():
    assert True

9.5 在代码中动态跳过

import pytest

def test_with_condition():
    database_available = check_database()

    if not database_available:
        pytest.skip("数据库不可用,跳过测试")

    # 实际测试代码
    assert True

10. 测试异常

10.1 使用 pytest.raises

测试代码是否抛出预期的异常:

import pytest

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

def test_divide_by_zero():
    """测试除以零是否抛出异常"""
    with pytest.raises(ValueError):
        divide(10, 0)

10.2 检查异常消息

import pytest

def test_exception_message():
    """测试异常消息"""
    with pytest.raises(ValueError) as exc_info:
        divide(10, 0)

    assert str(exc_info.value) == "除数不能为零"
    # 或者使用 match 参数
    with pytest.raises(ValueError, match="除数不能为零"):
        divide(10, 0)

10.3 使用正则表达式匹配

import pytest

def test_exception_with_regex():
    """使用正则表达式匹配异常消息"""
    with pytest.raises(ValueError, match=r"除数.*零"):
        divide(10, 0)

10.4 测试多种异常类型

import pytest

def risky_function(value):
    if value < 0:
        raise ValueError("值不能为负")
    if value > 100:
        raise OverflowError("值太大")
    return value

def test_multiple_exceptions():
    """测试多种异常"""
    # 测试 ValueError
    with pytest.raises(ValueError):
        risky_function(-1)

    # 测试 OverflowError
    with pytest.raises(OverflowError):
        risky_function(101)

10.5 测试不抛出异常

def test_no_exception():
    """测试不抛出异常的情况"""
    try:
        result = divide(10, 2)
        assert result == 5
    except Exception as e:
        pytest.fail(f"意外的异常:{e}")

11. 参数化测试

11.1 什么是参数化测试

参数化测试允许你使用不同的参数多次运行同一个测试函数,减少重复代码。

11.2 基本用法

import pytest

@pytest.mark.parametrize("input,expected", [
    (1 + 1, 2),
    (2 + 2, 4),
    (3 + 3, 6),
])
def test_addition(input, expected):
    """参数化测试加法"""
    assert input == expected

运行结果

test_param.py::test_addition[2-2] PASSED
test_param.py::test_addition[4-4] PASSED
test_param.py::test_addition[6-6] PASSED

11.3 多参数示例

import pytest

@pytest.mark.parametrize("username,password,expected", [
    ("admin", "123456", True),
    ("user", "password", True),
    ("admin", "wrong", False),
    ("", "123456", False),
])
def test_login(username, password, expected):
    """参数化测试登录"""
    result = login(username, password)  # 假设有这个函数
    assert result == expected

11.4 使用 ID 标识测试用例

import pytest

@pytest.mark.parametrize("a,b,expected", [
    pytest.param(1, 2, 3, id="正数相加"),
    pytest.param(-1, -2, -3, id="负数相加"),
    pytest.param(0, 0, 0, id="零相加"),
])
def test_add_with_ids(a, b, expected):
    """带 ID 的参数化测试"""
    assert a + b == expected

运行结果

test_param.py::test_add_with_ids[正数相加] PASSED
test_param.py::test_add_with_ids[负数相加] PASSED
test_param.py::test_add_with_ids[零相加] PASSED

11.5 组合多个参数化

import pytest

@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
    """组合参数化"""
    print(f"x={x}, y={y}")
    assert x * y > 0

# 这会生成 4 个测试:
# (1, 10), (1, 20), (2, 10), (2, 20)

11.6 从文件读取参数

import pytest
import json

# 从 JSON 文件读取测试数据
def load_test_data():
    with open("test_data.json", "r") as f:
        return json.load(f)

@pytest.mark.parametrize("username,password,expected", load_test_data())
def test_login_from_file(username, password, expected):
    """从文件读取参数"""
    result = login(username, password)
    assert result == expected

12. 简单的 Fixture 入门

12.1 什么是 Fixture

Fixture 是 pytest 中用于准备测试数据和环境的强大机制。它可以在测试之前设置所需的数据,并在测试之后进行清理。

12.2 基本用法

import pytest

@pytest.fixture
def sample_data():
    """提供测试数据的 fixture"""
    return {"name": "张三", "age": 25}

def test_user_name(sample_data):
    """使用 fixture"""
    assert sample_data["name"] == "张三"

def test_user_age(sample_data):
    """多个测试可以使用同一个 fixture"""
    assert sample_data["age"] == 25

12.3 Fixture 的 Setup 和 Teardown

使用 yield 分隔 setup 和 teardown 逻辑:

import pytest

@pytest.fixture
def database():
    """数据库 fixture"""
    # Setup:测试前执行
    print("n连接数据库...")
    db = DatabaseConnection()
    db.connect()

    yield db  # 返回 fixture 值给测试使用

    # Teardown:测试后执行
    print("n断开数据库...")
    db.disconnect()

def test_query(database):
    """使用数据库 fixture"""
    result = database.query("SELECT * FROM users")
    assert result is not None

12.4 Fixture 作用域

import pytest

@pytest.fixture(scope="function")  # 默认,每个测试函数都会执行
def function_scope():
    print("n函数级别 fixture")
    return "function"

@pytest.fixture(scope="class")  # 每个测试类执行一次
def class_scope():
    print("n类级别 fixture")
    return "class"

@pytest.fixture(scope="module")  # 每个模块执行一次
def module_scope():
    print("n模块级别 fixture")
    return "module"

@pytest.fixture(scope="session")  # 整个测试会话执行一次
def session_scope():
    print("n会话级别 fixture")
    return "session"

12.5 自动使用 Fixture

import pytest

@pytest.fixture(autouse=True)
def setup_logging():
    """自动应用的 fixture"""
    print("n开始测试...")
    yield
    print("n测试结束")

def test_1():
    assert True

def test_2():
    assert True

# 两个测试都会自动应用 setup_logging fixture

12.6 conftest.py 中共享 Fixture

conftest.py 文件中定义的 fixture 可以被同目录及子目录的测试使用:

conftest.py

import pytest

@pytest.fixture
def shared_data():
    """共享的 fixture"""
    return {"shared": True}

@pytest.fixture
def api_client():
    """API 客户端 fixture"""
    from api_client import APIClient
    return APIClient(base_url="https://api.example.com")

test_example.py

def test_with_shared_data(shared_data):
    """使用共享的 fixture"""
    assert shared_data["shared"] == True

def test_api(api_client):
    """使用 API 客户端 fixture"""
    response = api_client.get("/users")
    assert response.status_code == 200

13. 常用的 Pytest 命令

13.1 基础命令

# 运行所有测试
pytest

# 详细模式
pytest -v

# 显示 print 输出
pytest -s

# 组合:详细模式 + 显示 print
pytest -vs

# 遇到第一个失败就停止
pytest -x

# 运行上次失败的测试
pytest --lf

# 运行所有测试,但先运行上次失败的
pytest --ff

13.2 筛选测试

# 运行包含 "login" 的测试
pytest -k "login"

# 运行标记为 smoke 的测试
pytest -m smoke

# 只运行某个文件
pytest test_login.py

# 只运行某个函数
pytest test_login.py::test_user_login

# 只运行某个类
pytest test_login.py::TestLogin

13.3 输出控制

# 简洁输出
pytest -q

# 显示更详细的输出
pytest -vv

# 不显示错误回溯
pytest --tb=no

# 简短的错误回溯
pytest --tb=short

# 只显示一行错误信息
pytest --tb=line

13.4 测试收集

# 只收集测试,不运行
pytest --collect-only

# 显示收集的测试详情
pytest --collect-only -v

13.5 并行执行(需要 pytest-xdist)

# 使用 4 个进程并行执行
pytest -n 4

# 自动选择 CPU 数量
pytest -n auto

13.6 生成报告(需要 pytest-html)

# 生成 HTML 报告
pytest --html=report.html

# 生成独立的 HTML 报告(包含样式)
pytest --html=report.html --self-contained-html

14. 常见问题与解决方案

14.1 测试文件找不到

问题pytest 运行时提示 “no tests ran”

原因

  1. 文件名不符合 test_*.py*_test.py 规则
  2. 函数名不以 test_ 开头

解决方案

# ❌ 错误的文件名
login.py

# ✅ 正确的文件名
test_login.py

# ❌ 错误的函数名
def login():
    pass

# ✅ 正确的函数名
def test_login():
    pass

14.2 模块导入错误

问题ModuleNotFoundError: No module named 'xxx'

解决方案

  1. 在项目根目录添加 conftest.py(空文件即可)
  2. 使用相对导入或绝对导入
  3. 配置 Python 路径
# conftest.py(放在项目根目录)
import sys
import os

# 将项目根目录添加到 Python 路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

14.3 print 输出不显示

问题:测试中的 print() 语句没有输出

解决方案:使用 -s 参数

pytest -s test_example.py

14.4 类中有 init 方法导致测试不被发现

问题:测试类中有 __init__ 方法,测试不被收集

解决方案:使用 setup_method 或 fixture 代替 __init__

# ❌ 错误:使用 __init__
class TestUser:
    def __init__(self):
        self.user = User()

    def test_user(self):
        assert self.user is not None

# ✅ 正确:使用 setup_method
class TestUser:
    def setup_method(self):
        self.user = User()

    def test_user(self):
        assert self.user is not None

# ✅ 正确:使用 fixture
class TestUser:
    @pytest.fixture(autouse=True)
    def setup(self):
        self.user = User()

    def test_user(self):
        assert self.user is not None

14.5 测试顺序问题

问题:测试之间有依赖,需要按特定顺序执行

解决方案

  1. 安装 pytest-ordering 插件
  2. 使用 @pytest.mark.run(order=n) 指定顺序
pip install pytest-ordering
import pytest

@pytest.mark.run(order=1)
def test_first():
    pass

@pytest.mark.run(order=2)
def test_second():
    pass

14.6 中文乱码问题

问题:输出中文时出现乱码

解决方案

  1. 确保文件编码为 UTF-8
  2. 在文件开头添加编码声明
  3. Windows 下设置控制台编码
# -*- coding: utf-8 -*-

def test_chinese():
    message = "你好,世界!"
    assert "你好" in message

Windows 控制台:

chcp 65001
pytest -v

15. 最佳实践

15.1 测试命名

# ✅ 好的命名:描述性强
def test_user_login_with_valid_credentials_should_succeed():
    pass

def test_user_login_with_invalid_password_should_fail():
    pass

# ❌ 不好的命名:不够描述性
def test_1():
    pass

def test_login():
    pass

15.2 测试组织

# 使用类组织相关测试
class TestUserLogin:
    """用户登录相关测试"""

    def test_login_success(self):
        pass

    def test_login_wrong_password(self):
        pass

    def test_login_user_not_found(self):
        pass

class TestUserRegistration:
    """用户注册相关测试"""

    def test_register_success(self):
        pass

    def test_register_duplicate_email(self):
        pass

15.3 使用 Fixture 管理数据

import pytest

@pytest.fixture
def valid_user():
    return {"username": "testuser", "password": "password123"}

@pytest.fixture
def invalid_user():
    return {"username": "", "password": ""}

def test_login_with_valid_user(valid_user):
    result = login(valid_user["username"], valid_user["password"])
    assert result == True

def test_login_with_invalid_user(invalid_user):
    result = login(invalid_user["username"], invalid_user["password"])
    assert result == False

15.4 保持测试独立

# ✅ 好的做法:每个测试独立
def test_create_user():
    user = create_user("test@example.com")
    assert user is not None
    delete_user(user.id)  # 清理

def test_delete_user():
    user = create_user("delete@example.com")  # 自己准备数据
    result = delete_user(user.id)
    assert result == True

# ❌ 不好的做法:测试之间有依赖
user_id = None

def test_create_user():
    global user_id
    user = create_user("test@example.com")
    user_id = user.id
    assert user is not None

def test_delete_user():
    # 依赖 test_create_user 先执行
    result = delete_user(user_id)
    assert result == True

15.5 使用标记分类测试

import pytest

@pytest.mark.smoke
def test_home_page():
    """冒烟测试:首页"""
    pass

@pytest.mark.regression
def test_user_flow():
    """回归测试:用户流程"""
    pass

@pytest.mark.slow
def test_performance():
    """性能测试(慢)"""
    pass
# 只运行冒烟测试
pytest -m smoke

# 跳过慢速测试
pytest -m "not slow"

16. 总结

16.1 Pytest 核心概念回顾

  1. 测试发现:以 test_ 开头的文件和函数会被自动发现
  2. 断言:使用简单的 assert 语句进行验证
  3. Fixture:用于准备测试数据和环境
  4. 参数化:使用不同参数多次运行同一测试
  5. 标记:对测试进行分类和筛选

16.2 常用命令速查

命令 作用
pytest 运行所有测试
pytest -v 详细输出
pytest -s 显示 print
pytest -x 遇到失败停止
pytest -k "xxx" 按名称筛选
pytest -m xxx 按标记筛选
pytest --collect-only 只收集不运行

16.3 学习路线

  1. 入门阶段(本文档)

    • 安装 pytest
    • 编写第一个测试
    • 理解断言
    • 运行测试
  2. 进阶阶段

    • Fixture 深入学习
    • 参数化测试
    • 标记和跳过
    • conftest.py
  3. 高级阶段

    • 插件开发
    • 接口自动化
    • 与 CI/CD 集成
    • 测试报告生成

16.4 推荐的下一步

学完本入门教程后,建议按以下顺序继续学习:

  1. 01.pytest的用例发现规则.md – 深入理解测试发现机制
  2. 02.pytest中常用的参数.md – 掌握更多命令行参数
  3. 07.pytest的fixture.md – 深入学习 Fixture
  4. 05.pytest中数据驱动测试.md – 学习数据驱动测试

附录

A. Pytest 官方资源

B. 常用插件列表

插件 作用
pytest-html 生成 HTML 报告
pytest-xdist 并行执行测试
pytest-rerunfailures 失败重试
pytest-ordering 测试排序
pytest-cov 代码覆盖率
allure-pytest Allure 报告
pytest-mock Mock 支持
pytest-timeout 超时控制

C. 快速开始模板

project/conftest.py

import pytest

@pytest.fixture
def api_client():
    """API 客户端 fixture"""
    # 在此初始化 API 客户端
    pass

project/pytest.ini

[pytest]
python_files = test_*.py
python_functions = test_*
python_classes = Test*
testpaths = tests
addopts = -v --tb=short
markers =
    smoke: 冒烟测试
    slow: 慢速测试

project/tests/test_example.py

import pytest

class TestExample:
    def test_hello(self):
        assert True

    @pytest.mark.smoke
    def test_smoke(self):
        assert True

恭喜你完成了 Pytest 入门学习!

现在你已经掌握了 pytest 的基础知识,可以开始编写自己的测试用例了。记住,实践是最好的学习方式,多写测试、多运行、多调试,你会越来越熟练!

发表评论