•20 min read

Các mẫu Pytest Fixture và Tham số hóa nâng cao

Các mẫu Pytest Fixture và Tham số hóa nâng cao

Viết bài kiểm tra cho một tập lệnh Python nhỏ thì dễ. Còn kiểm tra một ứng dụng doanh nghiệp với hàng trăm endpoint microservice, tích hợp cơ sở dữ liệu và quy trình làm việc không đồng bộ? Đó là một vấn đề hoàn toàn khác. Khi các bộ kiểm thử phát triển, chúng thường trở thành một mớ hỗn độn của mã thiết lập dễ vỡ, thời gian thực thi chậm và những cơn ác mộng về trạng thái chia sẻ. May mắn thay, hệ thống fixture của pytest thực sự là phép màu một khi bạn hiểu nó. Nó sử dụng một mô hình tiêm phụ thuộc thông minh giúp tách rời hoàn toàn logic thiết lập của bạn khỏi các xác nhận của bạn. Bằng cách nắm vững phạm vi fixture, tham số hóa gián tiếp, các hook thu thập động và các mẫu teardown vững chắc, bạn có thể giữ cho bộ kiểm thử của bạn nhanh chóng và dễ bảo trì ngay cả khi nó mở rộng. Tôi đã thấy các nhóm giảm một nửa thời gian kiểm thử CI của họ chỉ bằng cách sửa phạm vi fixture của họ—nó thực sự tạo ra sự khác biệt lớn đến vậy.

Audio Briefing
0:00 / 0:00

Phạm vi Fixture kiểm soát vòng đời kiểm thử và chi phí bộ nhớ như thế nào?

Phạm vi fixture của Pytest kiểm soát vòng đời kiểm thử bằng cách quản lý tần suất thiết lập và teardown tài nguyên trên các ranh giới session, package, module, class và function.

Vòng đời phạm vi Fixture

Khi pytest thực thi một bộ kiểm thử, mọi fixture được yêu cầu bởi một hàm kiểm thử đều hoạt động trong một phạm vi vòng đời được chỉ định. Phạm vi mặc định, function, thực thi lại logic thiết lập và teardown fixture trước và sau mỗi trường hợp kiểm thử riêng lẻ. Mặc dù phạm vi function đảm bảo sự cô lập kiểm thử tuyệt đối, việc khởi tạo các tài nguyên nặng như container cơ sở dữ liệu hoặc trình điều khiển trình duyệt cho mỗi kiểm thử sẽ gây ra sự chậm lại đáng kể trong quá trình thực thi. Pytest cung cấp các phạm vi fixture rộng hơn (class, module, package và session) để tái sử dụng các tài nguyên đã khởi tạo trên nhiều lần thực thi kiểm thử, giảm đáng kể tổng thời gian chạy của bộ kiểm thử. Các nhóm QA cấu hình các bộ kiểm thử lớn phải cân bằng việc tái sử dụng phạm vi fixture với rủi ro cô lập trạng thái chia sẻ. Điều cần thiết là phải chọn ranh giới phạm vi cẩn thận để bạn không làm rò rỉ trạng thái giữa các trường hợp kiểm thử.

Hiểu cách các hệ thống phân cấp phạm vi fixture hoạt động trong một bộ kiểm thử thực tế giúp ngăn ngừa ô nhiễm trạng thái chia sẻ:

# conftest.py - Scoped Fixture Definitions
import pytest
import sqlite3
from typing import Generator

@pytest.fixture(scope="session")
def global_db_engine() -> Generator[sqlite3.Connection, None, None]:
    # Initialized once per test session run: High efficiency for heavy setup
    connection = sqlite3.connect(":memory:")
    connection.execute("CREATE TABLE users (id INT PRIMARY KEY, username TEXT)")
    yield connection
    # Teardown executes once at the end of the entire test session
    connection.close()

@pytest.fixture(scope="function")
def db_transaction(global_db_engine: sqlite3.Connection) -> Generator[sqlite3.Connection, None, None]:
    # Function scope wrapper guarantees isolation by rolling back changes per test
    global_db_engine.execute("BEGIN TRANSACTION")
    yield global_db_engine
    global_db_engine.execute("ROLLBACK")

Bảng dưới đây tóm tắt các đánh đổi hoạt động trên tất cả các cấp độ phạm vi fixture pytest được hỗ trợ:

Cấp độ phạm viTần suất khởi tạoTác động tốc độ thực thiĐảm bảo cô lậpTrường hợp sử dụng điển hình
functionMột lần cho mỗi hàm kiểm thửChậm nhấtCô lập tối đaMock trong bộ nhớ, đường dẫn tệp tạm thời
classMột lần cho mỗi lớp kiểm thửTrung bìnhCô lập trung bìnhCác xác nhận kiểm thử hợp đồng API được nhóm
moduleMột lần cho mỗi tệp kiểm thửNhanhCô lập thấpLược đồ bảng cơ sở dữ liệu cấp module
packageMột lần cho mỗi thư mục packageNhanh hơnCô lập thấpThiết lập máy khách dịch vụ đa module
sessionMột lần cho mỗi lần chạy kiểm thử đầy đủNhanh nhấtRủi ro trạng thái chia sẻContainer Docker, nhóm kết nối Redis

Việc chọn sự cân bằng phạm vi fixture phù hợp cho phép các nhóm duy trì sự cô lập kiểm thử nghiêm ngặt trong khi vẫn kiểm soát được tổng thời gian thực thi bộ kiểm thử. Nếu bạn không cô lập trạng thái kiểm thử đã thay đổi khi sử dụng các fixture có phạm vi session, các phụ thuộc thứ tự kiểm thử sẽ gây ra các lỗi kiểm thử không ổn định. Chúng tôi đã thấy các bộ kiểm thử thực thi nhanh hơn mười lần bằng cách tái sử dụng các container cơ sở dữ liệu có phạm vi session cùng với các rollback hàm giao dịch.

Ngoài ra, pytest cho phép các nhà phát triển định nghĩa phạm vi fixture động bằng cách sử dụng các hàm có thể gọi. Bằng cách truyền một hàm vào tham số scope của @pytest.fixture, pytest đánh giá các biến môi trường hoặc cờ dòng lệnh để xác định xem một fixture có nên chạy với phạm vi session hay function trong một lần chạy kiểm thử cụ thể hay không.

Ngoài ra, việc kết hợp các fixture container cơ sở dữ liệu có phạm vi session với các fixture rollback giao dịch có phạm vi function cung cấp sự kết hợp tối ưu giữa tốc độ thực thi và cô lập trạng thái.

Ngoài ra, fixture tmp_path của pytest cung cấp quản lý thư mục tạm thời có phạm vi tự động, đảm bảo rằng các tạo phẩm kiểm thử được tạo trong quá trình thực thi fixture được dọn dẹp sau khi kiểm thử hoàn tất.

Ngoài ra, việc định phạm vi các fixture ở cấp độ package cho phép chia sẻ các máy khách tích hợp đắt tiền trên tất cả các tệp kiểm thử trong một thư mục con microservice trong khi vẫn đảm bảo rằng các tài nguyên được dọn dẹp tự động khi thực thi chuyển sang một thư mục package khác.

Cuối cùng, việc quản lý phạm vi fixture đúng cách ngăn ngừa rò rỉ bộ nhớ trong các bộ kiểm thử lớn, đảm bảo rằng các tạo phẩm kiểm thử nặng được thu gom rác ngay sau khi module hoặc package chứa chúng hoàn tất thực thi.

Advertisement

Tham số hóa Fixture gián tiếp truyền đối số cho các Fixture phức tạp như thế nào?

Tham số hóa fixture gián tiếp truyền các giá trị tham số động trực tiếp vào các hàm fixture thông qua request.param trước khi bắt đầu thực thi hàm kiểm thử.

Luồng tham số hóa gián tiếp

@pytest.mark.parametrize tiêu chuẩn truyền các tham số kiểm thử trực tiếp vào các đối số hàm kiểm thử. Tuy nhiên, khi các trường hợp kiểm thử yêu cầu các đối tượng đã khởi tạo (chẳng hạn như các máy khách HTTP đã được cấu hình sẵn hoặc các mô hình cơ sở dữ liệu tùy chỉnh), việc truyền các giá trị thô trực tiếp vào các hàm kiểm thử buộc phải có logic khởi tạo boilerplate bên trong mỗi phần xác nhận kiểm thử. Tham số hóa gián tiếp cho phép các nhà phát triển tham số hóa một fixture thay vì, hướng dẫn pytest truyền các giá trị tham số đến thuộc tính request.param của hàm fixture trước khi thực thi trường hợp kiểm thử. Các kỹ sư tự động hóa kiểm thử sử dụng tham số hóa gián tiếp để giữ cho các phần xác nhận kiểm thử sạch sẽ và khai báo. Nếu bạn chưa áp dụng tham số hóa fixture gián tiếp, việc thiết lập trạng thái máy khách động bên trong mỗi hàm kiểm thử sẽ trùng lặp mã thiết lập trên các tệp kiểm thử.

Hãy xem xét một bộ kiểm thử ủy quyền đa vai trò trong đó các endpoint được đánh giá dựa trên các persona người dùng được xác thực khác nhau:

import pytest
from typing import NamedTuple

class UserProfile(NamedTuple):
    user_id: int
    role: str
    token: str

@pytest.fixture
def authenticated_client(request: pytest.FixtureRequest) -> dict[str, str]:
    # Extract indirect parameter passed via request.param
    role_type = getattr(request, "param", "guest")
    
    # Construct customized client context based on parameter configuration
    if role_type == "admin":
        return {"Authorization": "Bearer admin_jwt_token_secret", "role": "admin"}
    elif role_type == "editor":
        return {"Authorization": "Bearer editor_jwt_token_secret", "role": "editor"}
    return {"Authorization": "Bearer guest_public_token", "role": "guest"}

# Indirect parameterization passes arguments into authenticated_client fixture
@pytest.mark.parametrize(
    "authenticated_client, expected_status",
    [
        ("admin", 200),
        ("editor", 200),
        ("guest", 403),
    ],
    indirect=["authenticated_client"]
)
def test_admin_settings_access(authenticated_client: dict[str, str], expected_status: int) -> None:
    # Test function receives pre-configured fixture instance directly
    user_role = authenticated_client["role"]
    actual_status = 200 if user_role in ("admin", "editor") else 403
    assert actual_status == expected_status

Tham số hóa gián tiếp giữ cho các hàm xác nhận kiểm thử sạch sẽ và chỉ tập trung vào việc xác minh kết quả thay vì xây dựng logic thiết lập trạng thái kiểm thử. Nếu bạn không sử dụng tham số hóa gián tiếp, việc thiết lập trạng thái máy khách động bên trong mỗi trường hợp kiểm thử sẽ trùng lặp boilerplate thiết lập trên toàn bộ bộ kiểm thử của bạn. Bạn sẽ thấy rằng chi phí bảo trì kiểm thử giảm đáng kể khi logic xác nhận vẫn được cô lập khỏi tham số hóa thiết lập.

Ngoài ra, tham số hóa gián tiếp hỗ trợ truyền các cấu trúc từ điển phức tạp hoặc các đối tượng dữ liệu vào các nhà máy fixture. Khả năng này cho phép kiểm thử các endpoint API dựa trên các kết hợp payload đa dạng, cấu hình cơ sở dữ liệu hoặc phản hồi mock mà không làm lộn xộn các chữ ký kiểm thử.

Ngoài ra, các nhà phát triển có thể kết hợp tham số hóa gián tiếp với kế thừa fixture. Một fixture con có thể nhận các tham số gián tiếp, sửa đổi các payload dữ liệu và truyền các cấu hình đã cập nhật lên các fixture cha một cách sạch sẽ.

Ngoài ra, tham số hóa gián tiếp tích hợp với các cờ đánh dấu tùy chỉnh, cho phép các bộ kiểm thử lọc các nhóm thực thi kiểm thử dựa trên các giá trị tham số được truyền đến các fixture gián tiếp.

Ngoài ra, việc sử dụng tham số hóa gián tiếp với các lớp dữ liệu tùy chỉnh cho phép truyền các cấu hình kịch bản kiểm thử phức tạp vào các fixture mà không làm mất khả năng đọc chữ ký hàm kiểm thử.

Cuối cùng, việc sử dụng các ID tham số rõ ràng trong quá trình tham số hóa gián tiếp tạo ra đầu ra console dễ đọc trong các trình chạy kiểm thử, cho phép các nhà phát triển xác định ngay lập tức các kết hợp tham số bị lỗi.

Làm thế nào bạn có thể tạo các bộ kiểm thử động bằng cách sử dụng Pytest Generate Tests Hooks?

Bạn có thể tạo các bộ kiểm thử động bằng cách triển khai hook pytest_generate_tests trong conftest.py để tính toán các ma trận tham số tại thời điểm thu thập.

Tạo kiểm thử động

Trong khi @pytest.mark.parametrize xử lý tốt các danh sách tham số tĩnh, các bộ kiểm thử doanh nghiệp thường cần tạo các trường hợp kiểm thử động dựa trên các yếu tố bên ngoài như lược đồ cơ sở dữ liệu, tệp cấu hình ma trận hoặc cờ tính năng môi trường. Mã hóa cứng danh sách tham số bên trong các tệp kiểm thử dẫn đến các bộ kiểm thử lỗi thời khi cấu hình môi trường thay đổi. Hook pytest_generate_tests chạy trong giai đoạn thu thập kiểm thử của pytest, cho phép các nhà phát triển kiểm tra chữ ký hàm kiểm thử và tiêm các tập tham số đã tính toán một cách động. Các nhóm phát triển phần mềm xây dựng các đường ống kiểm thử dựa trên dữ liệu dựa vào các hook thu thập để tự động tạo các ma trận kiểm thử. Đừng buộc các nhà phát triển phải cập nhật các decorator tham số được mã hóa cứng khi các tệp ma trận bên ngoài thay đổi.

Đây là một triển khai conftest.py tạo động các trường hợp kiểm thử tương thích trình duyệt dựa trên các biến môi trường thời gian chạy:

# conftest.py - Dynamic Test Case Generator Hook
import os
import pytest

def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
    # Check if the target test function requests the dynamic 'browser_target' parameter
    if "browser_target" in metafunc.fixturenames:
        # Inspect environment variables to determine target test matrix
        env_matrix = os.getenv("TEST_BROWSER_MATRIX", "chromium,firefox").split(",")
        
        # Optionally customize test IDs for clear test runner console output
        test_ids = [f"browser_{b.strip()}" for b in env_matrix]
        
        # Inject calculated parameters dynamically into pytest collection pipeline
        metafunc.parametrize("browser_target", env_matrix, ids=test_ids)

Các hàm kiểm thử trong bộ yêu cầu browser_target như một tham số tiêu chuẩn, trong khi pytest xử lý việc tạo ma trận động đằng sau hậu trường:

# test_cross_browser.py
def test_rendering_engine_behavior(browser_target: str) -> None:
    # Function executes once per dynamically calculated browser parameter
    assert browser_target in ["chromium", "firefox", "webkit"]

Sử dụng các hook thu thập động cho phép các nhóm kỹ thuật điều khiển các kiểm thử tích hợp đa môi trường trực tiếp từ các ma trận cấu hình bên ngoài mà không cần sửa đổi mã kiểm thử. Nếu nhóm của bạn không tự động hóa việc tạo ma trận kiểm thử, việc thêm các môi trường kiểm thử mới yêu cầu cập nhật thủ công các tham số decorator trên hàng trăm tệp kiểm thử. Chúng tôi đang thấy các đường ống tích hợp liên tục sử dụng các hook thu thập để điều chỉnh mật độ ma trận kiểm thử dựa trên các nhánh đích của yêu cầu kéo.

Ngoài ra, pytest_generate_tests có thể kiểm tra các đánh dấu kiểm thử để tùy chỉnh các quy tắc tạo tham số cho mỗi module kiểm thử. Ví dụ, các kiểm thử được đánh dấu bằng @pytest.mark.slow có thể nhận các ma trận tập dữ liệu lớn hơn trong các lần chạy CI hàng đêm trong khi chạy các tập dữ liệu đã cắt bớt trong quá trình phát triển cục bộ.

Ngoài ra, các hook tạo động cho phép đọc dữ liệu kiểm thử trực tiếp từ các tệp CSV bên ngoài, bảng Parquet hoặc tệp đặc tả OpenAPI trong quá trình thu thập, tự động chuyển đổi các hàng dữ liệu thành các trường hợp kiểm thử pytest độc lập.

Ngoài ra, các hook thu thập động có thể đánh giá tên nhánh Git hiện tại để chọn các ma trận kiểm thử cụ thể, chạy các bộ hồi quy đầy đủ trên các nhánh phát hành chính trong khi thực hiện các kiểm thử khói được nhắm mục tiêu trên các nhánh tính năng.

Ngoài ra, việc tạo tham số động hỗ trợ lọc ra các kết hợp tham số không được hỗ trợ trước khi thu thập hoàn tất, ngăn chặn việc thực thi các kịch bản kiểm thử không hợp lệ.

Cuối cùng, việc tạo kiểm thử động tích hợp liền mạch với các plugin thực thi song song pytest như pytest-xdist, phân phối các trường hợp kiểm thử được tạo động đều trên các lõi CPU của worker.

Những cơ chế an toàn nào ngăn chặn các Teardown không ổn định trong Yield Fixtures?

Các cơ chế an toàn ngăn chặn các teardown không ổn định trong yield fixtures khi các nhà phát triển gói mã thiết lập trong các khối try-finally hoặc đăng ký các callback teardown với request.addfinalizer.

An toàn Teardown Fixture

Pytest yield fixtures tách logic thiết lập và teardown xung quanh một câu lệnh yield duy nhất. Mã trước yield chạy trong quá trình thiết lập fixture, trong khi mã sau yield thực thi trong quá trình teardown. Tuy nhiên, nếu một ngoại lệ xảy ra trong quá trình thực thi thiết lập trước khi đạt đến dòng yield, pytest sẽ bỏ qua hoàn toàn mã teardown sau yield. Điều này có thể để lại các tài nguyên còn sót lại, các socket chưa đóng hoặc các khóa cơ sở dữ liệu lơ lửng. Để đảm bảo rằng mã dọn dẹp thực thi đáng tin cậy bất kể lỗi thiết lập, các nhà phát triển nên sử dụng request.addfinalizer() hoặc gói các bước thiết lập trong các khối try-finally. Các kỹ sư độ tin cậy kiểm toán các bộ tự động hóa kiểm thử nhấn mạnh việc đăng ký finalizer để loại bỏ các lỗi teardown không ổn định. Nếu bạn chưa kiểm toán các trình xử lý dọn dẹp fixture của mình, các thiết lập bị lỗi sẽ để lại các khóa tiến trình mồ côi trên các trình chạy kiểm thử.

Ví dụ dưới đây so sánh một yield fixture không an toàn với một mẫu finalizer bền vững:

import pytest
import os
import tempfile
from typing import Generator

# Unsafe Yield Fixture: If setup fails mid-execution, temp file is never deleted
@pytest.fixture
def unsafe_temp_file() -> Generator[str, None, None]:
    fd, path = tempfile.mkstemp()
    # If an exception occurs here, the post-yield remove line never runs!
    os.write(fd, b"Initial setup data payload")
    os.close(fd)
    yield path
    os.remove(path)

# Safe Resilient Fixture using request.addfinalizer
@pytest.fixture
def safe_temp_file(request: pytest.FixtureRequest) -> str:
    fd, path = tempfile.mkstemp()
    os.close(fd)
    
    # Register finalizer immediately after resource allocation
    def cleanup() -> None:
        if os.path.exists(path):
            os.remove(path)
            print(f"Finalizer successfully removed temporary file at {path}")
            
    # Finalizer is guaranteed to execute even if setup raises an error later
    request.addfinalizer(cleanup)
    
    # Subsequent setup operations
    with open(path, "wb") as f:
        f.write(b"Safe setup data payload")
        
    return path

Đăng ký finalizer ngay sau khi cấp phát tài nguyên đảm bảo thực thi teardown sạch sẽ ngay cả khi các hoạt động thiết lập thất bại giữa chừng. Nếu bạn không đăng ký finalizer ngay sau khi cấp phát tài nguyên, các lỗi thiết lập một phần sẽ để lại các tệp tạm thời bị rò rỉ trên các máy chủ build. Đây là một thực hành độ tin cậy cơ bản cho các bộ tự động hóa kiểm thử quy mô lớn.

Ngoài ra, request.addfinalizer() hỗ trợ đăng ký nhiều hàm dọn dẹp cho một fixture duy nhất. Finalizer thực thi theo thứ tự đăng ký ngược (Last-In, First-Out), đảm bảo rằng các tài nguyên phụ thuộc được dọn dẹp sạch sẽ.

Ngoài ra, việc kết hợp finalizer với các trình quản lý ngữ cảnh (contextlib.ExitStack) cho phép các fixture quản lý nhiều tài nguyên tạm thời một cách an toàn mà không cần viết các khối try-finally lồng nhau.

Ngoài ra, việc tích hợp các câu lệnh ghi nhật ký bên trong các finalizer teardown giúp các nhà phát triển chẩn đoán lỗi dọn dẹp tài nguyên trong quá trình phân tích chạy tích hợp liên tục.

Ngoài ra, việc đăng ký finalizer bên trong các plugin pytest tùy chỉnh đảm bảo rằng các tài nguyên session kiểm thử toàn cầu (chẳng hạn như máy chủ HTTP mock hoặc container cơ sở dữ liệu kiểm thử) tắt một cách duyên dáng ngay cả khi các lần chạy kiểm thử bị hủy giữa bộ.

Cuối cùng, thiết kế teardown bền vững ngăn chặn các lỗi bộ kiểm thử theo tầng, trong đó một tài nguyên chưa được dọn dẹp trong một kiểm thử gây ra các kiểm thử độc lập tiếp theo bị lỗi trong quá trình thực thi. Thiết lập các chính sách teardown sạch sẽ trên tất cả các fixture kiểm thử đảm bảo rằng các bộ kiểm thử tích hợp liên tục chạy đáng tin cậy ngày này qua ngày khác.

Advertisement

Bạn cũng có thể thích

Các câu hỏi thường gặp về Pytest Fixtures

autouse=True của fixture khác với khai báo tham số fixture rõ ràng như thế nào?

Các fixture được định nghĩa với autouse=True tự động thực thi cho tất cả các kiểm thử trong phạm vi đã khai báo của chúng mà không yêu cầu các hàm kiểm thử phải yêu cầu chúng một cách rõ ràng. Mặc dù tiện lợi cho các thiết lập toàn cục, việc lạm dụng autouse có thể che khuất các phụ thuộc kiểm thử và làm chậm quá trình thực thi.

Điều gì xảy ra khi một hàm kiểm thử yêu cầu các fixture với các cấp độ phạm vi khác nhau?

Pytest giải quyết các đồ thị phụ thuộc fixture theo thứ tự độ rộng phạm vi: các fixture session thực thi trước, sau đó là các fixture có phạm vi package, module, class và cuối cùng là function.

Các yield fixture có thể trả về giá trị cho các hàm kiểm thử không?

Có, các yield fixture trả về giá trị được truyền cho câu lệnh yield cho các hàm kiểm thử gọi. Mã sau câu lệnh yield tiếp tục thực thi trong quá trình teardown sau khi trường hợp kiểm thử hoàn tất.

pytest.mark.usefixtures khác với việc truyền các fixture dưới dạng đối số hàm như thế nào?

Decorator @pytest.mark.usefixtures("fixture_name") áp dụng một fixture cho một hàm hoặc lớp kiểm thử mà không truyền giá trị trả về của fixture vào chữ ký hàm kiểm thử, làm cho nó lý tưởng cho các fixture chỉ thiết lập.

Các nhà phát triển có thể tham số hóa các fixture có phạm vi session trong pytest không?

Tham số hóa gián tiếp tiêu chuẩn bị hạn chế đối với các fixture có phạm vi session vì các giá trị tham số thường được tính toán cho mỗi hàm kiểm thử. Tuy nhiên, việc sử dụng các hook động như pytest_generate_tests cho phép quản lý tham số cấp session.

Các mẫu ghi đè fixture hoạt động như thế nào trong các tệp conftest.py lồng nhau?

Pytest cho phép các tệp conftest.py trong thư mục con ghi đè các fixture được định nghĩa trong các tệp conftest.py trong thư mục cha, cho phép các sub-package tùy chỉnh hành vi thiết lập cho các bộ kiểm thử cục bộ.

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement