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

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

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

pytest tests

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

几乎所有 unittest 功能都受支持

  • @unittest.skip 风格的装饰器;

  • setUp/tearDown;

  • setUpClass/tearDownClass;

  • setUpModule/tearDownModule;

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

截至目前,pytest 不支持以下功能

开箱即用的优势

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

pytest 在 unittest.TestCase 子类中的功能

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

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

第三方插件可能工作良好,也可能不工作良好,具体取决于插件和测试套件。

使用标记将 pytest fixture 混入 unittest.TestCase 子类

使用 pytest 运行您的 unittest 允许您将它的 fixture 机制unittest.TestCase 风格的测试一起使用。假设您至少已略读过 pytest fixture 功能,让我们直接进入一个示例,它集成了 pytest db_class 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 为每个类调用一次。由于故意失败的断言语句,我们可以在回溯中查看 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 实例,这正是我们上面编写类作用域 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 的测试的 setup 和 teardown 是在测试的 call 阶段执行的,而不是在 pytest 的标准 setupteardown 阶段执行的。在某些情况下,尤其是在分析错误时,理解这一点可能很重要。例如,如果一个基于 unittest 的套件在 setup 期间出现错误,pytest 在其 setup 阶段不会报告任何错误,而是在 call 期间抛出错误。