Pytest Fixtures是Pytest框架中用于管理测试前置条件和后置清理的一种强大工具。可以帮助我们在测试中设置初始状态,减少重复代码,并提高测试的可维护性


下载代码查看我的示例
git clone https://gitee.com/Ac1998/pytest-fixtures-demo.git
  • 1.
  • 2.

下面我们来学习一下如何使用它

前置条件

第 1 步 — 设置项目目录

使用 idea 创建一个项目并设置一个虚拟环境,以确保项目依赖项得到隔离和管理有效

首先创建一个目录,该目录将包含我们在此编写的代码教程:

pytest-fixtures-demo
  • 1.

之后,我们在终端查看是否已经激活虚拟环境,在本例中为:venv

(.venv) PS D:\python\pytest-fixtures-demo>
  • 1.

接下来,使用以下代码创建一个文件:app.py

class Library:
    def __init__(self):
        self.books: list[dict[str, str]] = []  # 初始化一个空的图书列表

    def add_book(self, title: str, author: str) -> str:
        # 添加一本新书到图书列表
        self.books.append({"title": title, "author": author})
        return "成功添加图书"

    def get_book(self, index: int) -> str:
        # 根据索引获取图书信息
        if 0 <= index < len(self.books):
            book = self.books[index]
            return f"标题: {book['title']}, 作者: {book['author']}"
        else:
            return "索引超出范围"

    def update_book(self, index: int, title: str, author: str) -> str:
        # 更新指定索引的图书信息
        if 0 <= index < len(self.books):
            self.books[index]["title"] = title
            self.books[index]["author"] = author
            return "图书更新成功"
        else:
            return "索引超出范围"

    def list_books(self) -> str:
        # 列出所有图书
        if not self.books:
            return "图书馆中没有图书"
        return "\n".join(
            f"标题: {book['title']}, 作者: {book['author']}" for book in self.books
        )

    def clear_books(self) -> None:
        # 清空所有图书
        self.books = []


# 使用示例:
if __name__ == "__main__":
    library = Library()  # 创建一个Library实例
    print(library.add_book("平凡的世界", "路遥"))  # 添加一本书
    print(library.add_book("许三观卖血记", "余华"))  # 添加另一本书
    print(library.update_book(0, "平凡的世界-1", "路遥"))  # 更新第一本书的信息
    print(library.list_books())  # 列出所有图书
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.

Library 类管理一系列书籍,它从一个空列表开始,并具有添加、检索、更新和列出书籍的方法

add_book() 方法添加一本书及其标题和作者

get_book() 方法按索引检索一本书

update_book() 方法根据书籍的索引更新书籍的详细信息

list_books() 方法列出图书馆中的所有书籍,而 clear_books() 方法清理列表


要为此类编写测试,请先安装 Pytest:

pip install -U pytest
  • 1.

创建一个 tests 目录来保存测试文件,这个目录直接创建为 Python包

之后,我们直接使用PyCharm。运行下面的测试脚本

from app import Library


def test_add_book():
    library = Library()

    library.add_book("老舍", "骆驼祥子")
    assert library.books == [
        {"title": "老舍", "author": "骆驼祥子"}
    ]


def test_get_book():
    library = Library()
    library.add_book("朝花夕拾", "鲁迅")
    assert library.get_book(0) == "标题: 朝花夕拾, 作者: 鲁迅"


def test_update_book():
    library = Library()
    library.add_book("朝花夕拾", "鲁迅")
    library.update_book(0, "朝花夕拾-01", "鲁迅")

    assert library.books[0] == {
        "title": "朝花夕拾-01",
        "author": "鲁迅",
    }


def test_list_books():
    library = Library()
    library.add_book("水浒传", "施耐庵")
    library.add_book("三国演义", "罗贯中")
    assert library.list_books() == (
        "title: 水浒传, author: 施耐庵\n"
        "title: 三国演义, author: 罗贯中"
    )
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.

使用以下命令运行测试:

pytest -v
  • 1.

输出将如下所示:

tests/test_library.py::test_add_book PASSED                              [ 25%]
tests/test_library.py::test_get_book PASSED                              [ 50%]
tests/test_library.py::test_update_book PASSED                           [ 75%]
tests/test_library.py::test_list_books PASSED                            [100%]
============================== 4 passed in 0.01s ======================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

虽然测试结果是正确的,但测试本身是有问题的。主要是Library 类的实例中有重复的,如果 Library 类需要新的实例化参数,则需要更新所有测试以包含新参数

为避免这种重复,我们可以使用固定装置


第 2 步 — 开始使用 Pytest Fixture

要了解如何使用 Pytest Fixture,让我们看一下四步过程 对于编写测试用例:

  • 安排:通过设置对象、服务、 数据库记录、URL、凭据或测试所需的任何条件
  • 操作:执行要测试的操作,例如调用函数
  • 断言:验证操作的结果是否符合您的预期
  • 清理:恢复环境,避免影响其他测试

Pytest Fixture在 安排 步骤中发挥作用。这些固定装置是以字符串、数字、字典或对象的形式返回数据的函数,测试函数或方法可以使用这些数据。你可以告诉Pytest,函数是带有@pytest.fixture装饰器的固定装置


可以像下面这样:

import pytest

@pytest.fixture
def numbers():
    return [1, 2, 3]


def test_sum_numbers(numbers):
    assert sum(numbers) == 6
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

numbers 固定装置返回一个列表 [1, 2, 3],该列表在 test_sum_numbers 函数中用于验证列表的总和


我们可以通过定义两个fixtures将相同的概念应用于程序,创建返回 Library() 实例和包含书籍的实例的固定装置

复制test_library.py文件命名为test_library_fixture.py方便区分

import pytest

from app import Library


@pytest.fixture
def library():
    return Library()


@pytest.fixture
def library_with_books():
    library = Library()
    library.add_book("水浒传", "施耐庵")
    library.add_book("三国演义", "罗贯中")
    return library


def test_add_book(library):
    library.add_book("老舍", "骆驼祥子")
    assert library.books == [
        {"title": "老舍", "author": "骆驼祥子"}
    ]


def test_get_book(library):
    library.add_book("朝花夕拾", "鲁迅")
    assert library.get_book(0) == "标题: 朝花夕拾, 作者: 鲁迅"


def test_update_book(library_with_books):
    library_with_books.update_book(0, "朝花夕拾-01", "鲁迅")

    assert library.books[0] == {
        "title": "朝花夕拾-01",
        "author": "鲁迅",
    }


def test_list_books(library_with_books):
    """ 这个使用 第二个 fixture """
    assert library_with_books.list_books() == (
        "title: 水浒传, author: 施耐庵\n"
        "title: 三国演义, author: 罗贯中"
    )
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

现在,每个测试用例都使用这些固定装置来获取必要的Library 实例了

使用夹具的好处包括提高代码的可读性和可维护性,因为设置逻辑是集中的,并且可以在多个测试中重复使用

此外,fixtures 将测试设置和执行明确分离,使测试更易于理解和修改

这种方法还允许更灵活和模块化的测试,因为可以使用不同的fixtures 轻松配置和测试不同的初始状态


保存新更改并运行测试:

(.venv) D:\caijinwei-python\pytest-fixtures-demo git:[main]
pytest -v
================================================================= test session starts ==================================================================
platform win32 -- Python 3.11.5, pytest-8.3.2, pluggy-1.5.0 -- D:\caijinwei-python\pytest-fixtures-demo\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\caijinwei-python\pytest-fixtures-demo
collected 8 items                                                                                                                                       

tests/test_library.py::test_add_book PASSED                                                                                                       [ 12%]
tests/test_library.py::test_get_book PASSED                                                                                                       [ 25%]
tests/test_library.py::test_update_book PASSED                                                                                                    [ 37%]
tests/test_library.py::test_list_books PASSED                                                                                                     [ 50%]
tests/test_library_fixture.py::test_add_book PASSED                                                                                               [ 62%]
tests/test_library_fixture.py::test_get_book PASSED                                                                                               [ 75%]
tests/test_library_fixture.py::test_update_book PASSED                                                                                            [ 87%]
tests/test_library_fixture.py::test_list_books PASSED                                                                                             [100%]

================================================================== 8 passed in 0.02s ===================================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

这样一来,测试就通过了。现在我们应该已经基本了解了fixtures 的工作原理以及它们带来的好处,然后我们可以继续下一部分,从其他fixtures 请求fixtures


第 3 步 — 从其他固定装置请求固定装置

Pytest 中的fixtures 是模块化的,允许一个fixtures 使用另一个fixtures

例如,可以重写library_with_books 以调用library(),如下所示:

...

@pytest.fixture
def library():
    return Library()


@pytest.fixture
def library_with_books(library):
    library.add_book("水浒传", "施耐庵")
    library.add_book("三国演义", "罗贯中")
    return library
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

现在我们再次运行测试:

将会看到测试顺利通过:

(.venv) D:\caijinwei-python\pytest-fixtures-demo git:[main]
pytest -v
================================================================= test session starts ==================================================================
platform win32 -- Python 3.11.5, pytest-8.3.2, pluggy-1.5.0 -- D:\caijinwei-python\pytest-fixtures-demo\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\caijinwei-python\pytest-fixtures-demo
collected 8 items                                                                                                                                       

tests/test_library.py::test_add_book PASSED                                                                                                       [ 12%]
tests/test_library.py::test_get_book PASSED                                                                                                       [ 25%]
tests/test_library.py::test_update_book PASSED                                                                                                    [ 37%]
tests/test_library.py::test_list_books PASSED                                                                                                     [ 50%]
tests/test_library_fixture.py::test_add_book PASSED                                                                                               [ 62%]
tests/test_library_fixture.py::test_get_book PASSED                                                                                               [ 75%]
tests/test_library_fixture.py::test_update_book PASSED                                                                                            [ 87%]
tests/test_library_fixture.py::test_list_books PASSED                                                                                             [100%]

================================================================== 8 passed in 0.02s ===================================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

在其他fixtures 中使用fixtures 是一个很有用的功能。但是,重要的是要谨慎对待fixtures 依赖性,确保基础装置(library)中的更改不会无意中影响从属fixtures 。适当的状态管理对于避免测试剥落至关重要,确保每个测试都具有干净且独立的状态,以保持可靠和可预测的测试结果。


第 4 步 — 在多个文件中使用fixturesconftest.py文件

随着项目的发展,使用相同fixtures 的多个测试文件是很常见的。在每个测试文件中设置fixtures 效率不高。一个好的解决方案是创建一个 conftest.py 文件并在其中添加fixtures 。Pytest 将自动发现这些fixtures ,并将它们用于所有测试文件,而无需导入它们

为此,我们可以在tests 目录中的 conftest.py 文件内定义固定装置,如下所示:

tests/
├── conftest.py
├── test_library.py
├── test_library_fixture.py
app.py
main.py
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

一旦定义了fixtures ,所有测试文件,包括子目录中的文件,都将能够使用fixtures


接下来,我们将所有的导入和固定装置移动到 conftest.py文件中,如下所示:

import pytest

from app import Library


@pytest.fixture
def library():
    return Library()


@pytest.fixture
def library_with_books(library):
    library.add_book("水浒传", "施耐庵")
    library.add_book("三国演义", "罗贯中")
    return library
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

通过该更改,tests/test_library_fixture.py文件仅包含测试函数

保存更改后,再次运行测试。即使测试文件没有任何fixtures 导入,用例也可以正常运行:

输出:

(.venv) D:\caijinwei-python\pytest-fixtures-demo git:[main]
pytest -v
================================================================= test session starts ==================================================================
platform win32 -- Python 3.11.5, pytest-8.3.2, pluggy-1.5.0 -- D:\caijinwei-python\pytest-fixtures-demo\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\caijinwei-python\pytest-fixtures-demo
collected 8 items                                                                                                                                       

tests/test_library.py::test_add_book PASSED                                                                                                       [ 12%]
tests/test_library.py::test_get_book PASSED                                                                                                       [ 25%]
tests/test_library.py::test_update_book PASSED                                                                                                    [ 37%]
tests/test_library.py::test_list_books PASSED                                                                                                     [ 50%]
tests/test_library_fixture.py::test_add_book PASSED                                                                                               [ 62%]
tests/test_library_fixture.py::test_get_book PASSED                                                                                               [ 75%]
tests/test_library_fixture.py::test_update_book PASSED                                                                                            [ 87%]
tests/test_library_fixture.py::test_list_books PASSED                                                                                             [100%]

================================================================== 8 passed in 0.00s ===================================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

使用 conftest.py时,请确保所有测试文件都可以访问相同的fixtures ,而无需冗余导入。


第 5 步 — 了解 Pytest fixtures 范围

在 Pytest 中,fixtures 是设置测试所需资源的强大方法。 fixtures 的一个关键特征是它们的范围,它决定了fixtures 的长度 将处于活动状态以及何时设置和拆除。


以下是从最低到最高范围排序的范围:

  • 功能范围:默认范围。fixtures 在设置之前 每个测试功能并在测试功能完成后拆除。
  • 类范围:该fixtures 为每个测试类别设置一次,可用于 该类中的所有测试方法。
  • 模块范围:该fixtures 为每个测试模块设置一次,并且可用 到该模块中的所有测试功能。
  • 包装范围:该fixtures 为每个包装设置一次,可用于 该包中的所有测试。
  • 会话范围:该fixtures 在整个测试会话中设置一次,并且是 可用于在该会话期间运行的所有测试

首先,打开文件:conftest.py

用单个固定装置替换文件的内容:conftest.py 并注销其他脚本

import pytest

from app import Library


@pytest.fixture
def library():
    lib = Library()
    lib.add_book("水浒传", "施耐庵")
    lib.add_book("三国演义", "罗贯中")
    yield lib
    lib.clear_books()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

接下来,打开测试文件:

清除所有内容并添加以下修改后的测试以使用单个fixtures

def test_add_book(library):
    library.add_book("红楼梦", "曹雪芹")
    expected_books = [
        {"title": "红楼梦", "author": "曹雪芹"},
        {"title": "水浒传", "author": "施耐庵"},
        {"title": "三国演义", "author": "罗贯中"},
    ]
    print("Actual books:", sorted(library.books, key=lambda x: x["title"]))
    print("Expected books:", sorted(expected_books, key=lambda x: x["title"]))
    assert sorted(library.books, key=lambda x: x["title"]) == sorted(
        expected_books, key=lambda x: x["title"]
    )


def test_get_book(library):
    library.add_book("一九八四", "乔治·奥威尔")
    assert library.get_book(2) == "标题: 一九八四, 作者: 乔治·奥威尔"


def test_update_book(library):
    library.update_book(0, "朝花夕拾-01", "鲁迅")

    assert library.books[0] == {
        "title": "朝花夕拾-01",
        "author": "鲁迅",
    }


def test_list_books(library):
    """ 这个使用 第二个 fixture """
    assert library.list_books() == (
        "title: 水浒传, author: 施耐庵\n"
        "title: 三国演义, author: 罗贯中"
    )
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

这里的主要变化是test_add_book使用library固定装置。为了确保正确添加书籍,测试会检查现有书籍和新添加的书籍是否存在

若要确保测试运行正确,请再次运行测试脚本:

输出应如下所示:

(.venv) D:\caijinwei-python\pytest-fixtures-demo git:[main]
pytest -v
================================================================= test session starts ==================================================================
platform win32 -- Python 3.11.5, pytest-8.3.2, pluggy-1.5.0 -- D:\caijinwei-python\pytest-fixtures-demo\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\caijinwei-python\pytest-fixtures-demo
collected 4 items                                                                                                                                       

tests/test_library_single_fixture.py::test_add_book PASSED                                                                                        [ 25%]
tests/test_library_single_fixture.py::test_get_book PASSED                                                                                        [ 50%]
tests/test_library_single_fixture.py::test_update_book PASSED                                                                                     [ 75%]
tests/test_library_single_fixture.py::test_list_books PASSED                                                                                      [100%]

================================================================== 4 passed in 0.02s ===================================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

但是,有时候我们不希望所有测试都在一个文件中,因为很难理解作用域之间的差异,特别是当涉及到modulepackagesession作用域时。因此,我们将测试文件分成两个文件

首先,创建一个测试文件,该文件将只有两种方法用于测试是否可以添加和检索书籍:

将以下代码添加到该文件中:

def test_add_book(library):
    library.add_book("红楼梦", "曹雪芹")
    expected_books = [
        {"title": "红楼梦", "author": "曹雪芹"},
        {"title": "水浒传", "author": "施耐庵"},
        {"title": "三国演义", "author": "罗贯中"},
    ]
    print("Actual books:", sorted(library.books, key=lambda x: x["title"]))
    print("Expected books:", sorted(expected_books, key=lambda x: x["title"]))
    assert sorted(library.books, key=lambda x: x["title"]) == sorted(
        expected_books, key=lambda x: x["title"]
    )


def test_get_book(library):
    library.add_book("一九八四", "乔治·奥威尔")
    assert library.get_book(2) == "标题: 一九八四, 作者: 乔治·奥威尔"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

接下来,创建第二个测试文件,用于测试更新书籍并列出所有书籍:

def test_update_book(library):
    library.update_book(0, "朝花夕拾-01", "鲁迅")

    assert library.books[0] == {
        "title": "朝花夕拾-01",
        "author": "鲁迅",
    }


def test_list_books(library):
    """ 这个使用 第二个 fixture """
    assert library.list_books() == (
        "title: 水浒传, author: 施耐庵\n"
        "title: 三国演义, author: 罗贯中"
    )
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

删除文件:test_library.py

为确保一切运行正常,请重新运行 Pytest 命令:

输出应如下所示:

(.venv) D:\caijinwei-python\pytest-fixtures-demo git:[main]
pytest -v
================================================================= test session starts ==================================================================
platform win32 -- Python 3.11.5, pytest-8.3.2, pluggy-1.5.0 -- D:\caijinwei-python\pytest-fixtures-demo\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\caijinwei-python\pytest-fixtures-demo
collected 4 items                                                                                                                                       

tests/test_library_management.py::test_update_book PASSED                                                                                         [ 25%]
tests/test_library_management.py::test_list_books PASSED                                                                                          [ 50%]
tests/test_library_operations.py::test_add_book PASSED                                                                                            [ 75%]
tests/test_library_operations.py::test_get_book PASSED                                                                                            [100%]

================================================================== 4 passed in 0.02s ===================================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.


功能范围 Function scope

function scope是默认范围。这意味着为每个测试功能设置fixtures 并在每次测试后销毁。因此,我们见过的任何示例都使用了function作用域。这确保了每个测试都具有干净的状态

使用--setup-show标志再次运行测试,以观察设置和拆卸过程:

pytest -v --setup-show
  • 1.

输出如下所示:

(.venv) D:\caijinwei-python\pytest-fixtures-demo git:[main]
pytest -v --setup-show
================================================================= test session starts ==================================================================
platform win32 -- Python 3.11.5, pytest-8.3.2, pluggy-1.5.0 -- D:\caijinwei-python\pytest-fixtures-demo\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\caijinwei-python\pytest-fixtures-demo
collected 4 items                                                                                                                                       

tests/test_library_management.py::test_update_book 
SETUP    F library
tests/test_library_management.py::test_update_book (used: library)PASSED
TEARDOWN F library
tests/test_library_management.py::test_list_books 
SETUP    F library
tests/test_library_management.py::test_list_books (used: library)PASSED
TEARDOWN F library
tests/test_library_operations.py::test_add_book 
SETUP    F library
tests/test_library_operations.py::test_add_book (used: library)PASSED
TEARDOWN F library
tests/test_library_operations.py::test_get_book 
SETUP    F library
tests/test_library_operations.py::test_get_book (used: library)PASSED
TEARDOWN F library

    ================================================================== 4 passed in 0.02s ===================================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

SETUP F libraryTEARDOWN F library行表明fixtures 在每次测试之前设置并在每次测试后拆除,确保每次测试的新鲜状态。这里, F代表function作用域,这是Pytest中的默认作用域。这意味着fixtures 针对每个测试功能单独应用

这种方法确保测试之间的隔离,防止共享状态带来的副作用。但是,如果我们要设置数据库连接等资源,则成本可能会很高,因为没有真正的理由为每个测试持续创建新的连接


模块范围 Module scope

Python 中的模块是可以使用import关键字导入的单个文件。 Pytest 允许我们的固定装置定义模块范围 module范围允许模块内的所有测试共享固定装置,从而确保一致的状态。这意味着该装置实例化一次,并在模块中测试代码的整个执行过程中持续存在。当最后一个测试执行时,fixtures 将被销毁


此范围通常适用于以下场景:

  • 设置或拆卸过程涉及资源密集型操作,例如数据库连接或加载特定于模块的配置
  • 性能得到提高,因为它减少了重复设置和拆卸,因为实例在需要共享fixtures 的所有测试中都是重复使用的

但是,如果我们的测试修改了fixtures ,则可能会导致测试之间的不一致。因此,只有当我们确定状态保持不变,或者如果已修改,其他测试可以处理它时,才应使用此方法

要了解模块作用域如何处理多个文件,现在我们打开 :conftest.py 并将范围设置为:module

@pytest.fixture(scope="module")

def library():
    ...
  • 1.
  • 2.
  • 3.
  • 4.

现在使用以下命令运行测试:

pytest -v --setup-show
  • 1.

输出将显示以下内容:

(.venv) D:\caijinwei-python\pytest-fixtures-demo git:[main]
pytest -v --setup-show
================================================================= test session starts ==================================================================
platform win32 -- Python 3.11.5, pytest-8.3.2, pluggy-1.5.0 -- D:\caijinwei-python\pytest-fixtures-demo\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\caijinwei-python\pytest-fixtures-demo
collected 4 items                                                                                                                                       

tests/test_library_management.py::test_update_book 
SETUP    M library
tests/test_library_management.py::test_update_book (fixtures used: library)PASSED
tests/test_library_management.py::test_list_books 
tests/test_library_management.py::test_list_books (fixtures used: library)FAILED
TEARDOWN M library
tests/test_library_operations.py::test_add_book 
SETUP    M library
tests/test_library_operations.py::test_add_book (fixtures used: library)PASSED
tests/test_library_operations.py::test_get_book 
tests/test_library_operations.py::test_get_book (fixtures used: library)FAILED
TEARDOWN M library

    ======================================================================= FAILURES =======================================================================
    ___________________________________________________________________ test_list_books ____________________________________________________________________

library = <app.Library object at 0x000001CAFB7FAD50>

    def test_list_books(library):
""" 这个使用 第二个 fixture """
>       assert library.list_books() == (
    "title: 水浒传, author: 施耐庵\n"
    "title: 三国演义, author: 罗贯中"
)
E       AssertionError: assert 'title: 朝花夕拾-..., author: 罗贯中' == 'title: 水浒传, ..., author: 罗贯中'
E
E         - title: 水浒传, author: 施耐庵
E         + title: 朝花夕拾-01, author: 鲁迅
E           title: 三国演义, author: 罗贯中

tests\test_library_management.py:12: AssertionError
____________________________________________________________________ test_get_book _____________________________________________________________________ 

library = <app.Library object at 0x000001CAFB7FB310>

def test_get_book(library):
    library.add_book("一九八四", "乔治·奥威尔")
>       assert library.get_book(2) == "标题: 一九八四, 作者: 乔治·奥威尔"
E       AssertionError: assert '标题: 红楼梦, 作者: 曹雪芹' == '标题: 一九八四, 作者: 乔治·奥威尔'
E
E         - 标题: 一九八四, 作者: 乔治·奥威尔
E         + 标题: 红楼梦, 作者: 曹雪芹

tests\test_library_operations.py:17: AssertionError
=============================================================== short test summary info ================================================================ 
FAILED tests/test_library_management.py::test_list_books - AssertionError: assert 'title: 朝花夕拾-..., author: 罗贯中' == 'title: 水浒传, ..., author:  罗贯中'
FAILED tests/test_library_operations.py::test_get_book - AssertionError: assert '标题: 红楼梦, 作者: 曹雪芹' == '标题: 一九八四, 作者: 乔治·奥威尔'      
============================================================= 2 failed, 2 passed in 0.05s ==============================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.

在输出中,我们可以看到对于每个模块(每个测试文件)中的所有测试,设置和拆卸都用SETUP M libraryTEARDOWN M library包装。这意味着夹具在模块中的任何测试运行之前设置一次,并在模块中的所有测试完成后拆除


这减少了为每个测试创建和拆除库的开销,提供更高效的设置,同时仍然确保不同测试文件之间的隔离

但是,由于fixtures 在模块内共享,因此在一次测试中对其状态所做的更改可能会影响后续测试。这就是为什么某些测试可能会失败的原因,因为它们假设fixtures 以干净状态启动,但事实并非如此


包装范围 Package scope

package范围对于在同一包内的多个模块之间共享固定装置非常有用。 package范围固定装置在包的开头(包含__init__.py文件的目录)创建,并在该包内的所有子目录(包)之间共享。然后,在封装中最后一次测试的拆卸过程中,fixtures 被破坏掉


若要应用范围,请更新文件:

@pytest.fixture(scope="package")
def library():
    ...
  • 1.
  • 2.
  • 3.

现在使用以下命令重新运行测试:

pytest -v --setup-show
  • 1.

输出将显示:

tests/test_library_management.py::test_update_book 
  SETUP    P library
        tests/test_library_management.py::test_update_book (fixtures used: library)PASSED
tests/test_library_management.py::test_list_books 
        tests/test_library_management.py::test_list_books (fixtures used: library)FAILED
tests/test_library_operations.py::test_add_book 
        tests/test_library_operations.py::test_add_book (fixtures used: library)FAILED
tests/test_library_operations.py::test_get_book 
        tests/test_library_operations.py::test_get_book (fixtures used: library)FAILED
  TEARDOWN P library
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

在此输出中,我们可以看到fixtures 在包的开头设置一次( SETUP P library )并在末尾拆除( TEARDOWN P library )。因此,一旦test_update_bookfixtures 进行更新,所有其他测试都会失败,因为它们的编写方式期望fixtures 是一个干净的,但事实并非如此

就像模块作用域一样,使用包作用域时确保测试不会修改固定装置非常重要;否则,后续测试可能会因状态变化而失败


会话范围 Session scope

session范围是另一个范围,当我们希望函数共享相同的测试设置时,它会很有帮助。 session范围固定装置在测试运行开始时创建,并在所有测试完成后销毁。它们在整个测试会话中持续存在,这使得它们对于为整个测试会话设置昂贵的资源(例如数据库连接)的装置很有用

将范围设置为:session

@pytest.fixture(scope="session")
def library():
    ...
  • 1.
  • 2.
  • 3.

使用以下命令运行测试:

pytest -v --setup-show
  • 1.
collected 4 items                                                                                                                                       

tests/test_library_management.py::test_update_book 
SETUP    S library
        tests/test_library_management.py::test_update_book (fixtures used: library)PASSED
tests/test_library_management.py::test_list_books 
        tests/test_library_management.py::test_list_books (fixtures used: library)FAILED
tests/test_library_operations.py::test_add_book 
        tests/test_library_operations.py::test_add_book (fixtures used: library)FAILED
tests/test_library_operations.py::test_get_book 
        tests/test_library_operations.py::test_get_book (fixtures used: library)FAILED
TEARDOWN S library
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

在会话范围内, library装置在整个测试会话开始时设置一次,并在所有测试完成后拆除


此范围对于最小化设置和拆卸操作最为有效,因为它在整个测试会话中仅执行一次这些操作,无论涉及多少测试包或模块


第 6 步 — 参数化fixtures

在Python中,使用fixtures 的函数可以被参数化以编写简洁和 可读的测试。使用这些装置的测试函数被多次调用, 每次都使用不同的参数执行


当处理各种数据库连接值、多个文件等情况下,这种方法是有益的

要使用参数化,需要将params关键字参数传递给带有一组输入值的固定装置装饰器,如下所示: @pytest.fixture(params=[values...])

要了解其工作原理,请创建一个文件:test_parametrization.py

code tests/test_parametrization.py
  • 1.

添加以下代码,该代码使用 Pytest 的夹具参数化:

import pytest

@pytest.fixture(params=["image_1.jpg", "document_1.pdf", "image_2.png", "image_3.jpeg"])
def original_file_path(request):
    return request.param

def convert_to_hyphens(file_path):
    return file_path.replace("_", "-")

def test_convert_to_hyphens(original_file_path):
    converted_file_path = convert_to_hyphens(original_file_path)
    assert "-" in converted_file_path
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

original_file_path()装置提供params列表中的各种文件路径。 test_convert_to_hyphens()测试依赖于此固定装置并运行多次,针对列表中的每个文件路径运行一次。这确保了对convert_to_hyphens()函数的全面测试,该函数用连字符替换下划线

要执行测试,请运行:

pytest tests/test_parametrization.py -v
  • 1.

输出将显示类似于以下内容的输出:

(.venv) D:\caijinwei-python\pytest-fixtures-demo git:[main]
pytest .\tests\test_parametrization.py -v
================================================== test session starts ===================================================
platform win32 -- Python 3.11.5, pytest-8.3.2, pluggy-1.5.0 -- D:\caijinwei-python\pytest-fixtures-demo\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\caijinwei-python\pytest-fixtures-demo
collected 4 items                                                                                                         

tests/test_parametrization.py::test_convert_to_hyphens[image_1.jpg] PASSED                                          [ 25%]
tests/test_parametrization.py::test_convert_to_hyphens[document_1.pdf] PASSED                                       [ 50%]
tests/test_parametrization.py::test_convert_to_hyphens[image_2.png] PASSED                                          [ 75%]
tests/test_parametrization.py::test_convert_to_hyphens[image_3.jpeg] PASSED                                         [100%]

=================================================== 4 passed in 0.01s ====================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

此输出显示测试函数运行了四次,每次都具有不同 输入。

有了这个,我们就可以有效地参数化fixtures


第 7 步 — 使用内置Fixtures

Pytest 附带涵盖常见测试场景的内置固定装置。这些装置通过减少样板代码并通过标准功能确保一致性来简化测试的编写和维护

  •  monkeypatch: 临时修改函数、类、字典等。
  •  Request: 提供有关请求夹具的测试功能的信息。
  •  TMPDIR: 返回对每个测试函数唯一的临时目录路径对象。
  •  tmp_path_factory: 返回公共基本临时目录下的临时目录。
  •  recwarn: 记录测试函数发出的警告。
  •  Capsys: 捕获对 sys.stdout 和 sys.stderr 的写入

在该步骤让我们看一下tmp_path Fixtures 。此装置对于管理临时目录至关重要,我们可以避免使用真实目录,由于各种平台(Windows、macOS、Unix)上的文件路径不同,真实目录可能会很复杂。tmp_path 固定装置提供了一种受控、隔离且与平台无关的目录管理方法,具有自动设置和拆卸功能


首先我们来创建一个文件:test_builtin_fixtures.py

然后添加以下代码:

def test_create_and_verify_temp_file(tmp_path):

    # 创建临时目录
    temporary_directory = tmp_path / "example_temp_dir"
    temporary_directory.mkdir()

    # 创建临时文件并写入内容
    temporary_file = temporary_directory / "example_file.txt"
    temporary_file.write_text("临时文件内容")

    # 断言临时文件存在
    assert temporary_file.is_file()

    # 断言临时文件内容正确
    assert temporary_file.read_text() == "临时文件内容"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

此代码使用tmp_path创建一个临时目录和文件。 它验证文件的存在和内容,确保测试 环境在不同平台上是隔离且一致的

运行该文件:

pytest tests/test_builtin_fixtures.py -v --setup-show
  • 1.

输出如下所示:

tests/test_builtin_fixtures.py::test_create_and_verify_temp_file 
SETUP    S tmp_path_factory
        SETUP    F tmp_path (fixtures used: tmp_path_factory)
        tests/test_builtin_fixtures.py::test_create_and_verify_temp_file (fixtures used: request, tmp_path, tmp_path_factory)PASSED
        TEARDOWN F tmp_path
TEARDOWN S tmp_path_factory

=================================================== 1 passed in 0.02s ====================================================
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

如果不使用tmp_path,测试会很复杂:

import tempfile
import os
import pytest


def create_and_verify_temp_file(temp_dir):
    # 在临时目录中创建文件
    temp_file_path = os.path.join(temp_dir, "example_file.txt")

    with open(temp_file_path, "w") as temp_file:
        temp_file.write("临时文件内容")

    # 验证文件是否存在
    assert os.path.isfile(temp_file_path)

    # 验证文件内容
    with open(temp_file_path, "r") as temp_file:
        content = temp_file.read()
        assert content == "临时文件内容"


def test_create_and_verify_temp_file_without_fixture():
    # 创建临时目录
    with tempfile.TemporaryDirectory() as temp_dir:
        # 调用函数创建并验证文件
        create_and_verify_temp_file(temp_dir)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

此代码不仅难阅读,而且冗长。使用Fixtures 可以简化这一点,并使测试更具可读性和可维护性


如果内置的固定装置还不够,Pytest 还有很广泛的第三方插件列表。其中一些插件提供了有用的Fixtures 。使用它们可以直接 pip进行安装

pip install <package_name>
  • 1.

相信我如果你能够看完这篇文章并进行实际脚本的编写一定能够对你有所帮助!