如何在 pytest 中使用基于 unittest 的测试

pytest 支持开箱即用运行基于 Python unittest 的测试。它旨在利用现有的基于 unittest 的测试套件,使用 pytest 作为测试运行器,并允许逐步调整测试套件以充分利用 pytest 的功能。

要使用 pytest 运行现有的 unittest 风格的测试套件,请键入

pytest tests

pytest 将自动收集 test_*.py*_test.py 文件中的 unittest.TestCase 子类及其 test 方法。

几乎所有 unittest 功能都受支持

到目前为止,pytest 不支持以下功能

开箱即用的优点

通过使用 pytest 运行测试套件,您可以利用多项功能,在大多数情况下无需修改现有代码

unittest.TestCase 子类中的 pytest 功能

以下 pytest 功能在 unittest.TestCase 子类中有效

由于设计理念不同,以下 pytest 功能无效,并且可能永远无效

第三方插件可能或可能无法正常工作,具体取决于插件和测试套件。

使用标记将 pytest fixture 混合到 unittest.TestCase 子类中

使用 pytest 运行您的 unittest 允许您在 unittest.TestCase 风格的测试中使用其fixture 机制。假设您至少已经大致了解了 pytest fixture 功能,让我们直接看一个将 pytest db_class fixture 集成到其中的示例,该 fixture 设置了一个类缓存的数据库对象,然后从 unittest 风格的测试中引用它

# content of conftest.py

# we define a fixture function below and it will be "used" by
# referencing its name from tests

import pytest


@pytest.fixture(scope="class")
def db_class(request):
    class DummyDB:
        pass

    # set a class attribute on the invoking test context
    request.cls.db = DummyDB()

这定义了一个 fixture 函数 db_class,如果使用它,它将为每个测试类调用一次,并将类级别的 db 属性设置为 DummyDB 实例。fixture 函数通过接收一个特殊的 request 对象来实现这一点,该对象可以访问请求的测试上下文,例如 cls 属性,表示使用 fixture 的类。这种架构将 fixture 的编写与实际的测试代码解耦,并允许通过最少的引用(fixture 名称)重用 fixture。因此,让我们编写一个使用我们的 fixture 定义的实际 unittest.TestCase

# content of test_unittest_db.py

import unittest

import pytest


@pytest.mark.usefixtures("db_class")
class MyTest(unittest.TestCase):
    def test_method1(self):
        assert hasattr(self, "db")
        assert 0, self.db  # fail for demo purposes

    def test_method2(self):
        assert 0, self.db  # fail for demo purposes

@pytest.mark.usefixtures("db_class") 类装饰器确保 pytest fixture 函数 db_class 每个类调用一次。由于故意失败的 assert 语句,我们可以查看回溯中的 self.db

$ pytest test_unittest_db.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

test_unittest_db.py FF                                               [100%]

================================= FAILURES =================================
___________________________ MyTest.test_method1 ____________________________

self = <test_unittest_db.MyTest testMethod=test_method1>

    def test_method1(self):
        assert hasattr(self, "db")
>       assert 0, self.db  # fail for demo purposes
        ^^^^^^^^^^^^^^^^^
E       AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E       assert 0

test_unittest_db.py:11: AssertionError
___________________________ MyTest.test_method2 ____________________________

self = <test_unittest_db.MyTest testMethod=test_method2>

    def test_method2(self):
>       assert 0, self.db  # fail for demo purposes
        ^^^^^^^^^^^^^^^^^
E       AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E       assert 0

test_unittest_db.py:14: AssertionError
========================= short test summary info ==========================
FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft...
FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft...
============================ 2 failed in 0.12s =============================

此默认 pytest 回溯显示两个测试方法共享相同的 self.db 实例,这正是我们编写上述类作用域 fixture 函数时的意图。

使用 autouse fixture 并访问其他 fixture

虽然通常最好明确声明给定测试所需的 fixture 的使用,但有时您可能希望拥有在给定上下文中自动使用的 fixture。毕竟,传统的 unittest 设置风格强制使用这种隐式 fixture 编写,而且您很可能习惯了它或喜欢它。

您可以用 @pytest.fixture(autouse=True) 标记 fixture 函数,并在您希望使用它的上下文中定义 fixture 函数。让我们看一个 initdir fixture,它使 TestCase 类的所有测试方法在一个预初始化了 samplefile.ini 的临时目录中执行。我们的 initdir fixture 本身使用 pytest 内置的 tmp_path fixture 来委托创建每个测试的临时目录

# content of test_unittest_cleandir.py
import unittest

import pytest


class MyTest(unittest.TestCase):
    @pytest.fixture(autouse=True)
    def initdir(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)  # change to pytest-provided temporary directory
        tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")

    def test_method(self):
        with open("samplefile.ini", encoding="utf-8") as f:
            s = f.read()
        assert "testdata" in s

由于 autouse 标志,initdir fixture 函数将用于定义它的类的所有方法。这是在类上使用 @pytest.mark.usefixtures("initdir") 标记的快捷方式,如前一个示例所示。

运行此测试模块…

$ pytest -q test_unittest_cleandir.py
.                                                                    [100%]
1 passed in 0.12s

… 得到一个通过的测试,因为 initdir fixture 函数在 test_method 之前执行。

注意

unittest.TestCase 方法不能直接接收 fixture 参数,因为实现这一点可能会影响运行通用 unittest.TestCase 测试套件的能力。

上面的 usefixturesautouse 示例应该有助于将 pytest fixture 混合到 unittest 套件中。

您还可以逐渐放弃从 unittest.TestCase 继承,转而使用普通断言,然后逐步开始受益于完整的 pytest 功能集。

注意

由于两个框架之间的架构差异,基于 unittest 的测试的设置和拆卸是在测试的 call 阶段执行的,而不是在 pytest 的标准 setupteardown 阶段执行的。这在某些情况下可能很重要,尤其是在推断错误时。例如,如果基于 unittest 的套件在设置过程中出现错误,pytest 将在其 setup 阶段不报告任何错误,而是在 call 阶段引发错误。