如何使用基于 unittest 的测试与 pytest

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

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

pytest tests

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

几乎所有 unittest 功能都受支持

  • @unittest.skip 样式装饰器;

  • setUp/tearDown;

  • setUpClass/tearDownClass;

  • setUpModule/tearDownModule;

此外,子测试pytest-subtests 插件支持。

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

开箱即用的好处

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

unittest.TestCase 子类中的 pytest 功能

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

以下 pytest 功能起作用,并且由于不同的设计理念,可能永远不会起作用

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

使用标记将 pytest 固定装置混合到 unittest.TestCase 子类中

使用 pytest 运行 unittest 时,您可以将其 固定装置机制unittest.TestCase 样式测试结合使用。假设您至少浏览过 pytest 固定装置功能,那么让我们直接跳到一个示例中,该示例集成了一个 pytest db_class 固定装置,设置一个类缓存的数据库对象,然后从 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()

这定义了一个固定装置函数 db_class,如果使用,则对每个测试类调用一次,并将类级别的 db 属性设置为 DummyDB 实例。固定装置函数通过接收一个特殊的 request 对象来实现此目的,该对象可以访问 请求的测试上下文,例如 cls 属性,表示使用固定装置的类。此架构将固定装置编写与实际测试代码解耦,并允许通过最少的引用(固定装置名称)重复使用固定装置。因此,让我们使用我们的固定装置定义编写一个实际的 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 固定装置函数 db_class。由于故意失败的 assert 语句,我们可以在回溯中查看 self.db

$ pytest test_unittest_db.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.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 实例,这是我们在编写上述类范围固定装置函数时的意图。

使用 autouse 固定装置并访问其他固定装置

虽然通常最好明确声明您对特定测试所需的固定装置,但有时您可能希望在给定上下文中自动使用固定装置。毕竟,unittest 设置的传统样式要求使用此隐式固定装置编写,并且您可能已经习惯或喜欢它。

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

# 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 固定装置函数将用于定义它的类的所有方法。这相当于在类上使用 @pytest.mark.usefixtures("initdir") 标记,就像前面的示例中一样。

运行此测试模块 …

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

… 为我们提供了一个通过的测试,因为 initdir 固定装置函数在 test_method 之前执行。

注意

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

上面的 usefixturesautouse 示例应有助于将 pytest 固定参数混合到 unittest 套件中。

您还可以逐渐放弃从 unittest.TestCase 子类化到纯断言,然后逐步开始受益于完整的 pytest 功能集。

注意

由于这两个框架之间的架构差异,unittest 基于测试的设置和拆除是在测试的 call 阶段执行的,而不是在 pytest 的标准 setupteardown 阶段执行的。在某些情况下,了解这一点很重要,尤其是在推理错误时。例如,如果 unittest 基于套件在设置期间出现错误,pytest 将在其 setup 阶段报告没有错误,而是在 call 期间引发错误。