Kỹ Thuật Viết Plugin Và Mở Rộng Pytest

Ghi Giới Thiệu Về Plugin Pytest

Bạn hoàn toàn có thể triển khai các plugin ngay trong dự án của mình dưới dạng file conftest hoặc tạo ra các gói cài đặt riêng biệt để sử dụng ở nhiều nơi, bao gồm cả các dự án bên thứ ba. Nếu mục tiêu của bạn chỉ là sử dụng mà không muốn đi sâu vào phát triển, hãy tham khảo hướng dẫn về cách cài đặt và kích hoạt plugin.

Một plugin thường bao gồm một hoặc nhiều hàm thực thi các phương thức hook. Các quy tắc cơ bản về việc viết các hàm hook này đã được giải thích chi tiết trong phần tài liệu liên quan. Hệ thống pytest dựa vào các plugin thông qua các điểm kết nối (hooks) được chỉ định để xử lý mọi khía cạnh từ cấu hình, thu thập dữ liệu, chạy bài kiểm tra đến báo cáo kết quả:

  • Plugin tích hợp sẵn: Được nạp từ thư mục nội bộ _pytest.
  • Plugin bên ngoài: Là các công cụ của bên thứ ba đã cài đặt, được phát hiện qua metadata đóng gói.
  • Plugin conftest.py: Các module được tìm thấy tự động trong các thư mục chứa bài kiểm tra.

Nguyên tắc chung là mỗi lần gọi hàm hook giống như một cuộc gọi hàm Python theo tỷ lệ 1:N, với N là số lượng hàm thực hiện được đăng ký cho yêu cầu đó. Tất cả các định nghĩa và triển khai đều tuân thủ quy ước tiền tố tên pytest_, giúp việc nhận diện trở nên dễ dàng.

Quy Trình Khám Phá Plugin Khi Khởi Động

Khi công cụ khởi động, pytest sẽ tải các module plugin theo trình tự sau:

  • Tải tất cả các plugin tích hợp sẵn.
  • Tải các plugin đã được đăng ký thông qua entry point của setuptools.
  • Quét trước các tùy chọn dòng lệnh và nạp các plugin được chỉ định bởi tham số -p hoặc --plugins trước khi phân tích cú pháp đầy đủ.
  • Suy luận và nạp tất cả các file conftest.py thông qua việc gọi đường dẫn lệnh:
    • Nếu không chỉ định đường dẫn kiểm tra cụ thể, nó sẽ dùng thư mục hiện tại.
    • Nạp conftest.py và các file tương đối so với thư mục đầu tiên chứa test.
  • Đệ quy nạp các plugin được chỉ định trong biến pytest_plugins nằm trong các file conftest.py.

Conftest.py: Plugin Dựa Trên Thư Mục Cục Bộ

Các plugin conftest.py địa phương chứa các triển khai hook phụ thuộc vào vị trí thư mục. Các hoạt động phiên làm việc (Session) và chạy test sẽ gọi các hook được định nghĩa trong các file conftest.py gần gốc hệ thống tệp nhất. Ví dụ dưới đây minh họa việc triển khai hook pytest_runtest_logfinish để chỉ hiển thị log trong thư mục con b:

b/conftest.py:

def pytest_runtest_logfinish(nodeid, location):
    # Chỉ thực hiện trong thư mục 'b'
    print("Kết thúc test:", nodeid)
b/test_unit.py:

def test_case_in_b():
    pass
a/test_other.py:

def test_case_in_a():
    pass

Cách thức thực thi:

pytest a/test_other.py --capture=no  # Không hiển thị log
pytest b/test_unit.py --capture=no  # Hiển thị log
Lưu ý: Nếu file conftest.py không nằm trong một gói Python (thư mục thiếu __init__.py), việc import conftest có thể gây mơ hồ do xung đột đường dẫn PYTHONPATH hoặc sys.path. Do đó, tốt nhất là đặt conftest.py trong phạm vi gói hoặc tránh import trực tiếp từ file này.

Xây Dựng Plugin Riêng

Để bắt đầu viết plugin, bạn có thể tham khảo các ví dụ thực tế như plugin thu thập tùy chỉnh dựa trên YAML, hoặc xem源码 các plugin nội bộ. Tất cả chúng đều mở rộng chức năng thông qua các hàm hook.

Mẹo: Sử dụng mẫu cookiecutter-pytest-plugin là một khởi đầu tuyệt vời. Nó cung cấp một plugin hoạt động, cấu hình tox, README chi tiết và entry point đã được thiết lập sẵn.

Cài Đặt Plugin Để Người Khác Có Thể Sử Dụng

Nếu bạn muốn chia sẻ plugin ra bên ngoài, bạn cần định nghĩa một entry point để pytest nhận diện. Entry point hoạt động dựa trên cơ chế của setuptools. pytest tìm kiếm entry point pytest11 để phát hiện plugin mới. Bạn có thể cấu hình điều này trong file setup.py như sau:

# Ví dụ ./setup.py
from setuptools import setup

setup(
    name="data_validator_plugin",
    packages=["data_validator"],
    entry_points={
        "pytest11": [
            "validator = data_validator.main"
        ]
    },
    classifiers=[
        "Framework :: Pytest",
        "Programming Language :: Python :: 3"
    ],
)

Nếu cài đặt theo cách này, pytest sẽ tải data_validator.main như một plugin định nghĩa các hook.

Lưu ý: Đảm bảo thêm classifier "Framework :: Pytest" để plugin xuất hiện rõ ràng trên PyPI.

Xử Lý Lại Câu Lệnh Assert (Assertion Rewriting)

Một tính năng cốt lõi của pytest là khả năng hiển thị chi tiết nội dung biểu thức khi assert thất bại. Cơ chế này gọi là "Assertion Rewriting", nó sửa đổi AST (Abstract Syntax Tree) trước khi biên dịch sang bytecode thông qua một móc nhập (import hook) tuân theo PEP 302. Móc nhập này được cài đặt sớm khi khởi động pytest. Tuy nhiên, để tránh vấn đề byte-code không đồng nhất, nó chỉ重写 các module test hoặc plugin đang chạy, không áp dụng cho các module nghiệp vụ thông thường được import vào.

Nếu bạn có các hàm trợ giúp chứa assertions nằm trong các module khác ngoài plugin chính, bạn cần yêu cầu pytest thực hiện viết lại asserts cho module đó trước khi nó được import.

Hàm pytest.register_assert_rewrite() cho phép bạn đăng ký tên module hoặc package để viết lại. Điều này phải được thực hiện trước khi module bị import, thường là trong __init__.py của package.

  • Ném lỗi TypeError nếu tên module không phải chuỗi.

Ví dụ về cấu trúc package:

validator_plugin/__init__.py
validator_plugin/core.py
validator_plugin/utils.py

Trong setup.py:

setup(..., entry_points={"pytest11": ["vplugin = validator_plugin.core"]}, ...)

Trong trường hợp này, chỉ core.py được xử lý lại mặc định. Nếu utils.py cũng chứa assertions cần cải thiện thông báo lỗi, hãy thêm mã sau vào __init__.py:

import pytest

# Đăng ký để viết lại asserts trong module utils
pytest.register_assert_rewrite("validator_plugin.utils")

Yêu Cầu Hoặc Nạp Plugin Trong Test Module

Bạn có thể buộc tải một plugin từ trong file test hoặc conftest.py bằng cách khai báo biến pytest_plugins:

pytest_plugins = ["plugin_name_a", "plugin_name_b"]

Khi file load xong, các plugin được chỉ định cũng sẽ được nạp. Bất kỳ module nào cũng có thể trở thành plugin, kể cả các module hỗ trợ nội bộ ứng dụng:

pytest_plugins = "backend.test_helpers.extra_fixtures"

Biến pytest_plugins được xử lý đệ quy, nghĩa là nếu module được nạp cũng khai báo biến này, thì nội dung của nó cũng sẽ được nạp tiếp. Lưu ý rằng việc sử dụng biến này trong các file conftest.py không nằm ở thư mục gốc của bài kiểm tra không được khuyến khích vì nó ảnh hưởng đến toàn bộ cây thư mục và có thể gây nhầm lẫn.

Truy Cập Plugin Khác Theo Tên

Nếu một plugin cần giao tiếp hoặc lấy dữ liệu từ plugin khác, nó có thể truy xuất qua trình quản lý plugin:

target_plugin = config.pluginmanager.get_plugin("identifier_name")

Để xem danh sách tên của các plugin hiện có, hãy sử dụng tùy chọn dòng lệnh --trace-config.

Kiểm Thử Chính Plugin Của Bạn

Pytest đi kèm với plugin pytester chuyên dụng để viết kiểm thử cho chính các đoạn mã plugin. Mặc định, plugin này bị tắt, bạn cần kích hoạt nó.

Có thể thêm dòng sau vào conftest.py trong thư mục kiểm thử:

pytest_plugins = ["pytester"]

Hoặc dùng dòng lệnh pytest -p pytester. Điều này giúp bạn sử dụng fixture testdir để mô phỏng môi trường kiểm thử.

Dưới đây là ví dụ mô tả cách kiểm thử một plugin tạo ra fixture welcome_message.

Định nghĩa plugin (mock):

import pytest

def pytest_addoption(parser):
    group = parser.getgroup("custom_msg")
    group.addoption(
        "--prefix",
        action="store",
        dest="prefix",
        default="Chào mừng",
        help='Cụm từ chào đón mặc định.',
    )

@pytest.fixture
def welcome_message(request):
    prefix_val = request.config.getoption("prefix")

    def _msg(suffix=None):
        p = prefix_val if suffix is None else f"{prefix_val}-{suffix}"
        return f"[{p}]"

    return _msg

Test case sử dụng testdir:

def test_welcome_plugin(testdir):
    # Tạo file conftest tạm thời
    testdir.makeconftest("""
        import pytest

        @pytest.fixture
        def user_param():
            return "Alice"
    """)

    # Tạo file test tạm thời
    testdir.makepyfile("""
        def test_default(welcome_message):
            assert welcome_message() == "[Chào mừng]"

        def test_with_suffix(welcome_message):
            assert welcome_message("User") == "[Chào mừng-User]"

        def test_with_user_fixture(welcome_message, user_param):
             assert welcome_message(user_param) == "[Chào mừng-Alice]"
    """)

    # Chạy thử nghiệm
    result = testdir.runpytest()
    
    # Xác nhận kết quả
    result.assert_outcomes(passed=3)

Ngoài ra, bạn cũng có thể sao chép các ví dụ từ thư mục mẫu bằng cách cấu hình file pytest.ini và sử dụng testdir.copy_example để giảm thiểu việc viết mã lặp lại.

Thẻ: pytest python plugin-development testing-framework conftest

Đăng vào ngày 30 tháng 6 lúc 13:31