如何將基於 unittest 的測試與 pytest 搭配使用

pytest 支援開箱即用地執行基於 Python unittest 的測試。它旨在利用現有的基於 unittest 的測試套件,將 pytest 用作測試執行器,並允許逐步調整測試套件,以充分利用 pytest 的功能。

要使用 pytest 執行現有的 unittest 風格的測試套件,請輸入

pytest tests

pytest 將自動收集 unittest.TestCase 子類別及其 test 方法,這些方法位於 test_*.py*_test.py 檔案中。

幾乎所有 unittest 功能都受到支援

  • @unittest.skip 風格裝飾器;

  • setUp/tearDown;

  • setUpClass/tearDownClass;

  • setUpModule/tearDownModule;

此外,子測試pytest-subtests 插件支援。

到目前為止,pytest 尚不支援以下功能

開箱即用的優點

透過使用 pytest 執行您的測試套件,您可以利用多項功能,在大多數情況下無需修改現有程式碼

unittest.TestCase 子類別中的 pytest 功能

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

由於不同的設計理念,以下 pytest 功能不起作用,而且可能永遠不會起作用

第三方插件可能運作良好,也可能無法良好運作,具體取決於插件和測試套件。

使用標記將 pytest fixtures 混合到 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。因此,讓我們編寫一個實際的 unittest.TestCase 類別,使用我們的 fixture 定義

# 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-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 fixtures 和存取其他 fixtures

雖然通常最好明確聲明給定測試所需的 fixtures,但有時您可能希望擁有在給定上下文中自動使用的 fixtures。畢竟,傳統的 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 fixtures 混合到 unittest 套件中。

您也可以逐步擺脫從 unittest.TestCase 子類別化的方式,轉向純粹的 assert,然後逐步開始受益於完整的 pytest 功能集。

注意

由於兩個框架之間架構上的差異,基於 unittest 的測試的設定和拆解是在測試的 call 階段執行的,而不是在 pytest 的標準 setupteardown 階段。這在某些情況下可能很重要,尤其是在推理錯誤時。例如,如果基於 unittest 的套件在設定期間顯示錯誤,則 pytest 將在其 setup 階段報告沒有錯誤,而是在 call 期間引發錯誤。