pytest 是一个非常灵活且强大的测试框架,它支持简单的单元测试到复杂的功能测试。显著特点是其简洁的语法,可以无需继承 TestCase 类直接使用函数来编写测试用例,并通过 assert语句 进行断言。还支持参数化测试、插件系统以及丰富的 assertion rewriting 功能,使编写测试更加便捷和直观。我们可以使用它进行自动化测试。

pytest 的 Fixture 系统使我们能够定义一个通用的设置步骤,该步骤可以反复使用,就像使用普通函数一样。两个不同的测试可以请求同一个 Fixture,然后 pytest 会根据该 Fixture 为每个测试测试用例提供各自的结果。

这对于确保测试不会相互影响非常有用。我们可以使用此系统确保每个测试都获得自己的新数据,并从干净的状态开始,以便提供一致、可重复的结果。

PS: 本文基于pytest 8.3.3,建议使用PyCharm编写并运行测试套件,方便。

简单使用 @pytest.fixture 装饰器

可以在 Package/ 下创建 fixture_fun.py ,定义 Fixtures :

Project/
├── Config/
│   └── pytest.ini            # 自定义 mark 标记
├── Data/
│   └── parametrize.json      # 根据策略模块
├── Package/                  # 程序目录
│   ├── __init__.py           # 包初始化文件,可以定义一些变量或执行一些操作。当然里面什么都不写也可以。
│   ├── fixture_fun.py        # 定义Fixture,比如初始化数据、清理数据等操作
│   ├── module1.py            # 应用程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│   └── module2.py            # 应用程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
├── Test/                     # 测试用例目录
│   ├── __init__.py           # 包初始化文件
│   ├── test_module1.py       # 测试 module1 的测试用例
│   └── test_module2.py       # 测试 module2 的测试用例
├── Tools/                    # 工具目录
│   ├── __init__.py           # 包初始化文件
│   └── files_processor.py    # 文件处理工具
├── app.py                    # 项目启动文件
├── requirements.txt          # 项目依赖项列表
└── README.md                 # 项目说明文档

被 @pytest.fixture 装饰的函数可以作为测试用例请求,也就是钩子函数:

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest


# 返回 'x'
@pytest.fixture
def str_function():
    return "X"


# 返回 test 函数
@pytest.fixture
def function_function():
    def test(str_test):
        return str_test

    return test

在测试用例中使用 Fixtures :

# C:\PythonTest\Test\test_module1.py

from Package.fixture_fun import str_function, function_function

# 此时 str_function 是 "X"
def test_1(str_function):
    assert "X" == str_function


# 此时 function_function 是 test 函数
def test_2(function_function):
    assert "JZY" == function_function("JZY")
    assert "JZ" == function_function("JZ")
    assert "JCY" == function_function("JCY")

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 2 items

Config\test_module1.py::test_1 PASSED                                                                            [ 50%]
Config\test_module1.py::test_2 PASSED                                                                            [100%]

================================================= 2 passed in 0.01s ==================================================

灵活使用 Fixtures

多个测试用例请求同一个 Fixture:

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest


# 返回 "Hello Word!"
@pytest.fixture
def test():
    return "Hello Word!"

在测试用例中使用 Fixtures :

# C:\PythonTest\Test\test_module1.py
from Package.fixture_fun import test

def test_1(test):
    assert 11 == len(test)


def test_2(test):
    assert 'H' in test

def test_3(test):
    assert "Hello Word!" == test

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 3 items

Config\test_module1.py::test_1 PASSED                                                                           [ 33%]
Config\test_module1.py::test_2 PASSED                                                                           [ 66%]
Config\test_module1.py::test_3 PASSED                                                                           [100%]

================================================= 3 passed in 0.01s ==================================================

单测试用例请求多个 Fixture :

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest

@pytest.fixture
def one():
    return "J"


@pytest.fixture
def two():
    return "Z"


@pytest.fixture
def three():
    return "Y"

在测试用例中使用 Fixtures :

# C:\PythonTest\Test\test_module1.py
from Package.fixture_fun import one, two, three

def test_test(one, two, three):
    assert "JZY" == one+two+three

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 1 items

Config\test_module1.py::test_test PASSED                                                                        [100%]

================================================= 1 passed in 0.01s ==================================================

Fixtures 请求其它 Fixtures:

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest


@pytest.fixture
def before():
    return "JZY"


@pytest.fixture
def after(before):
    return before

在测试用例中使用 Fixtures :

# C:\PythonTest\Test\test_module1.py
from Package.fixture_fun import after, before

def test_test(after):
    assert "JZY" == after

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 1 items

Config\test_module1.py::test_test PASSED                                                                        [100%]

================================================= 1 passed in 0.01s ==================================================

自动请求 Fixtures :

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest

database = []

@pytest.fixture(autouse=True)  # autouse=True 时,pytest 会自动执行
def database_insert():
    database.append('JZY')
    return True

在测试用例中使用 Fixtures :

# C:\PythonTest\Test\test_module1.py
# database_insert 被引用就会执行
from Package.fixture_fun import database_insert, database

def test_test():
    assert database == ['JZY']

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 1 items

Config\test_module1.py::test_test PASSED                                                                        [100%]

================================================= 1 passed in 0.01s ==================================================

通过 @pytest.mark.usefixtures 请求 Fixtures :

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest

database = []

@pytest.fixture()
def database_insert():
    database.append('JZY')
    return True

在测试用例中使用 Fixtures :

# C:\PythonTest\Test\test_module1.py
from Package.fixture_fun import database_insert, database

@pytest.mark.usefixtures("database_insert")
def test_test():
    assert database == ['JZY']

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 1 items

Config\test_module1.py::test_test PASSED                                                                        [100%]

================================================= 1 passed in 0.01s ==================================================

注意: 使用 @pytest.mark.usefixtures 请求 Fixtures ,测试用例不会获取到 Fixtures 的返回值。

Fixtures 作用域

Fixtures 可以设置为 5 个不同的作用域,分别是:

  • session:整个测试会话(即所有模块)只会初始化一次 Fixtures 。
  • package:包(即目录)中只会初始化一次 Fixtures 。
  • module:模块中只会初始化一次 Fixtures 。
  • class:测试组中只会初始化一次 Fixtures 。
  • function:每个测试用例都会初始化一次 Fixtures 。

设置 Fixtures 作用域为 function :

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest


# 定义一个作用域为 function 的 fixture
@pytest.fixture(scope="function")
def my_list():
    return [1, 2, 3]

在测试用例中使用 Fixtures :

# C:\PythonTest\Test\test_module1.py
from Package.fixture_fun import my_list

def test_one(my_list):
    my_list.append(4)
    assert my_list == [1, 2, 3, 4]


def test_two(my_list):
    assert my_list == [1, 2, 3]


def test_three(my_list):
    assert my_list == [1, 2, 3]

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 3 items

Config\test_module1.py::test_one PASSED                                                                         [ 33%]
Config\test_module1.py::test_two PASSED                                                                         [ 66%]
Config\test_module1.py::test_three PASSED                                                                       [100%]

================================================= 3 passed in 0.01s ==================================================

每一个测试用例请求 Fixtures:my_list ,都会初始化 my_list 的值,每个测试用例接收的值都是 [1, 2, 3] 。

设置 Fixtures 作用域为 module :

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest


# 定义一个作用域为 module 的 fixture
@pytest.fixture(scope="module")
def my_list():
    return [1, 2, 3]

在测试用例中使用 Fixtures :

# C:\PythonTest\Test\test_module1.py
from Package.fixture_fun import my_list

def test_one(my_list):
    my_list.append(4)
    assert my_list == [1, 2, 3, 4]


def test_two(my_list):
    assert my_list == [1, 2, 3]


def test_three(my_list):
    assert my_list == [1, 2, 3]

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 1 items

Config\test_module1.py::test_one PASSED                                                                         [ 33%]
Config\test_module1.py::test_two FAILED                                                                         [ 66%]
Config\test_module1.py::test_three FAILED                                                                       [100%]

===================================================== FAILURES =======================================================
_____________________________________________________ test_two _______________________________________________________


my_list = [1, 2, 3, 4]

    def test_two(my_list):
>       assert my_list == [1, 2, 3]
E       AssertionError: assert [1, 2, 3, 4] == [1, 2, 3]
E         
E         Left contains one more item: 4
E         
E         Full diff:
E           [
E               1,
E               2,...
E         
E         ...Full output truncated (3 lines hidden), use '-vv' to show

Test\test_module1.py:10: AssertionError

_____________________________________________________ test_three ______________________________________________________

my_list = [1, 2, 3, 4]

    def test_three(my_list):
>       assert my_list == [1, 2, 3]
E       AssertionError: assert [1, 2, 3, 4] == [1, 2, 3]
E         
E         Left contains one more item: 4
E         
E         Full diff:
E           [
E               1,
E               2,...
E         
E         ...Full output truncated (3 lines hidden), use '-vv' to show

Test\test_module1.py:14: AssertionError
=============================================== short test summary info ==============================================
FAILED Config\test_module1.py::test_two - AssertionError: assert [1, 2, 3, 4] == ...
FAILED Config\test_module1.py::test_three - AssertionError: assert [1, 2, 3, 4] =...
============================================ 2 failed, 1 passed in 0.04s =============================================

由于 Fixtures:my_list 在整个模块中只会初始化一次,执行 test_one 后,my_list 的值:[1, 2, 3, 4] 被缓存,后续执行 test_two、test_three 时,my_list 的值都是:[1, 2, 3, 4] 。

其它作用域的逻辑与 module 一致,这里不再赘述。

使用 yield 进行资源管理

import pytest
import pymysql
from pymysql import cursors


@pytest.fixture
def db_connection():
    # 连接到数据库
    connection = pymysql.connect(host='IP', user='user', password='password', database='test',
                                 charset='utf8mb4', cursorclass=cursors.DictCursor)

    yield connection  # 返回数据库连接给测试函数

    # 关闭数据库连接,有需要还可以执行数据路清理操作
    connection.close()


def test_insert_user(db_connection):
    cursor = db_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
    db_connection.commit()

    cursor.execute("SELECT * FROM users")
    result = cursor.fetchall()
    assert result == [(1, 'Alice')]

yield 语句不仅返回值,还在测试完成后执行后续的代码,从而实现关闭数据库连接。除了数据库管理,yield 还可以在文件操作、环境变量设置、网络连接管理和模拟对象管理等场景下使用。

传递参数给 Fixtures

使用 mark 标记传递参数到 Fixtures

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest


@pytest.fixture
def fixt(request):
    """
    获取测试用例的标记数据,并返回
    """
    # 获取测试用例标记递的数据
    marker = request.node.get_closest_marker("user")
    if marker is None:
        # 未获取到标记,则返回None
        data = None
    else:
        # 获取到标记,则返回标记数据
        data = marker.args[0]

    return data

在测试用例中通过 mark 传递数据:

# C:\PythonTest\Test\test_module1.py

import pytest

from Package.fixture_fun import fixt

# 已在 pytest.ini 中配置了 user 标记
@pytest.mark.user(42)
def test_fixt(fixt):
    assert fixt == 42

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 1 items

Config\test_module1.py::test_fixt PASSED                                                                        [100%]

================================================= 1 passed in 0.01s ==================================================

定义 Fixture 时,参数化数据

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest


@pytest.fixture(params=["JZY", "one man", pytest.param("a good person", marks=pytest.mark.user)])
def fixt(request):
    """
    通过 request.param 获取参数值
    """
    return request.param

在测试用例中使用 Fixtures:

# C:\PythonTest\Test\test_module1.py
from Package.fixture_fun import fixt

defdef test_fixt(fixt):
    assert (fixt == "JZY") or (fixt == "one man") or (fixt == "a good person")

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 3 items

Config\test_module1.py::test_fixt[JZY] PASSED                                                                    [ 33%]
Config\test_module1.py::test_fixt[one man] PASSED                                                                [ 66%]
Config\test_module1.py::test_fixt[a good person] PASSED                                                          [100%]

================================================= 3 passed in 0.01s ==================================================

三个参数就是三个测试,也可以通过 -m user 只执行 a good person 这个参数。

使用 @pytest.mark.parametrize 对 Fixtures 进行参数化

在 fixture_fun.py 中定义 Fixtures :

# C:\PythonTest\Package\fixture_fun.py
import pytest


@pytest.fixture()
def one_param_fixture(request):
    """
    通过 request.param 获取参数值
    """
    return request.param

@pytest.fixture()
def two_fixture(one_param_fixture):
    return one_param_fixture

在测试用例中使用 Fixtures:

# C:\PythonTest\Test\test_module1.py
import pytest
from Package.fixture_fun import one_param_fixture, two_fixture


@pytest.mark.parametrize("one_param_fixture", [2, 3])
def test_one_param_fixture(one_param_fixture, two_fixture):
    assert one_param_fixture == two_fixture
	

python .\app.py ,运行结果:

================================================ test session starts ================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 2 items

Config\test_module1.py::test_one_param_fixture[2] PASSED                                                        [ 50%]
Config\test_module1.py::test_one_param_fixture[3] PASSED                                                        [100%]

================================================= 2 passed in 0.01s ==================================================

依赖 Fixtures 可以实现一些通用操作,比如设置和清理测试环境(清理数据库等)、初始化测试数据、参数化测试、依赖注入、并发测试和自定义测试报告等。

还有一些 Fixtures 的用法没有在文章中说明,感兴趣的读者可以访问 How to use fixtures 了解,里面有很详细的介绍,可以进阶学习。


THEEND



© 转载需要保留原始链接,未经明确许可,禁止商业使用。CC BY-NC-ND 4.0