Xác thực và thực thi hàm hook
Pytest sử dụng cơ chế hook để cho phép các plugin can thiệp vào quy trình chạy kiểm thử. Khi một hook được gọi, Pytest sẽ tìm tất cả các triển khai đã đăng ký phù hợp với định nghĩa chuẩn và thực hiện chúng theo thứ tự nhất định.
Ví dụ điển hình là pytest_collection_modifyitems(config, items), được gọi sau khi Pytest thu thập xong danh sách các test case. Khi bạn viết một plugin có chứa hàm này:
def pytest_collection_modifyitems(config, items):
# Thực hiện thay đổi trên danh sách test items
items[:] = [item for item in items if "slow" not in item.keywords]
Pytest sẽ chỉ truyền vào các tham số mà bạn khai báo — ở đây là config và items. Dù định nghĩa gốc có thể bao gồm thêm session, việc bỏ qua tham số không dùng đến vẫn an toàn. Điều này giúp đảm bảo tính tương thích ngược khi Pytest bổ sung tham số mới trong các phiên bản sau.
Lưu ý: Chỉ những hook liên quan trực tiếp đến việc chạy test (ví dụ pytest_runtest_*) mới được phép ném ra ngoại lệ. Các hook khác nếu gây lỗi sẽ làm gián đoạn toàn bộ quá trình thực thi.
Tùy chọn firstresult: Trả về kết quả đầu tiên hợp lệ
Một số hook được đánh dấu với firstresult=True, nghĩa là quá trình gọi sẽ dừng ngay khi gặp một plugin trả về giá trị khác None. Kết quả này sẽ được sử dụng làm đầu ra duy nhất của toàn bộ hook call.
Ví dụ, nếu hai plugin cùng triển khai một hook dạng firstresult=True:
- Plugin A trả về
"preferred"→ Pytest sử dụng giá trị này và bỏ qua Plugin B. - Plugin A trả về
None→ Plugin B vẫn được gọi.
Cơ chế này hữu ích khi cần xác định hành vi ưu tiên mà không yêu cầu tất cả plugin phải đồng thuận.
Sử dụng hookwrapper để bao bọc thực thi
Thêm từ phiên bản 2.7, hookwrapper=True cho phép một hàm hook đóng vai trò như một lớp bao quanh các triển khai khác. Hàm này phải là một generator và chỉ được yield đúng một lần.
Ví dụ minh họa cách đo thời gian thực thi một test function:
@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
start_time = time.time()
print(f"Bắt đầu thực thi: {pyfuncitem.name}")
outcome = yield
duration = time.time() - start_time
print(f"Kết thúc: {pyfuncitem.name}, Thời gian: {duration:.2f}s")
# Có thể thay đổi kết quả nếu cần
if outcome.excinfo is None:
original = outcome.get_result()
outcome.force_result(original + " [đã xử lý]")
Trước yield: thực hiện tác vụ tiền xử lý.
Sau yield: truy cập đối tượng outcome để kiểm tra kết quả hoặc lỗi, rồi thực hiện hậu xử lý.
Đối tượng outcome cung cấp các phương thức như get_result() (ném exception nếu thất bại) và force_result(value) để ghi đè kết quả trả về.
Thứ tự thực thi hook: tryfirst, trylast và hookwrapper
Khi nhiều plugin cùng triển khai một hook, thứ tự gọi phụ thuộc vào các tùy chọn cấu hình:
# Plugin A: ưu tiên thực thi sớm nhất
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
items.reverse()
# Plugin B: trì hoãn đến khi các plugin khác hoàn tất
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
items.sort(key=lambda x: x.name)
# Plugin C: bao bọc toàn bộ quá trình
@pytest.hookimpl(hookwrapper=True)
def pytest_collection_modifyitems(items):
print("Bắt đầu xử lý danh sách test")
yield
print("Hoàn tất xử lý danh sách test")
Thứ tự thực thi:
- Plugin C (hookwrapper) – phần trước
yield. - Plugin A (
tryfirst=True). - Plugin B (
trylast=True). - Plugin C – phần sau
yield.
Các hookwrapper luôn được ưu tiên thực thi đầu tiên (trước cả tryfirst), và hoàn tất cuối cùng.
Đăng ký hook mới cho plugin
Một plugin có thể khai báo các hook riêng để các plugin khác phản hồi. Việc này thực hiện qua add_hookspecs() trong giai đoạn đăng ký:
# myplugin/hooks.py
class MyHookSpec:
"""Định nghĩa các hook tùy chỉnh"""
def pytest_myfeature_start(self):
"""Gọi khi chức năng bắt đầu."""
# myplugin/main.py
def pytest_configure(config):
config.pluginmanager.add_hookspecs(MyHookSpec)
Sau khi đăng ký, bất kỳ plugin nào (kể cả conftest.py) có thể triển khai pytest_myfeature_start để tích hợp.
Kết hợp với plugin bên thứ ba
Khi muốn sử dụng hook từ một plugin tùy chọn (ví dụ pytest-xdist), nên tránh đăng ký trực tiếp để tránh lỗi khi plugin chưa cài đặt.
Giải pháp: Đăng ký trì hoãn dựa trên sự hiện diện của plugin:
class XDistIntegration:
def pytest_testnodedown(self, node, error):
print(f"Node {node} bị ngắt kết nối: {error}")
def pytest_configure(config):
if config.pluginmanager.hasplugin("xdist"):
config.pluginmanager.register(XDistIntegration())
Cách này đảm bảo hook chỉ được kích hoạt khi môi trường hỗ trợ, tăng độ tin cậy và dễ bảo trì.