Nâng Tầm Kiểm Thử Tự Động Với Pytest Fixture Và Parametrize: Đỉnh Cao Viết Test Gọn Sạch

Khi dự án phần mềm lớn dần lên, việc viết các file test unittest truyền thống thường dẫn đến tình trạng lặp đi lặp lại code khởi tạo (setup) và dọn dẹp (teardown) tài nguyên, cũng như việc phải viết hàng chục hàm test chỉ để kiểm tra các bộ dữ liệu khác nhau.
Để giải quyết triệt để vấn đề này, thư viện Pytest mang đến hai vũ khí hạng nặng: Fixtures (Quản lý ngữ cảnh thông minh) và Parametrize (Kiểm thử hàng loạt dữ liệu). Bài viết này sẽ giúp bạn làm chủ hai kỹ thuật cốt lõi này.
🟩 PHẦN 1: PYTEST FIXTURES (QUẢN LÝ NGỮ CẢNH TEST)
1. Khai báo với @pytest.fixture và Dependency Injection
Thay vì viết lặp lại các đoạn code khởi tạo dữ liệu mẫu (ví dụ: tạo một kết nối database, khởi tạo đối tượng user) ở từng hàm test, bạn định nghĩa chúng thành một hàm Fixture và dùng decorator @pytest.fixture.
Pytest sử dụng cơ chế Dependency Injection (Bơm phụ thuộc) cực kỳ thông minh: bạn chỉ cần đưa tên của fixture vào làm tham số đầu vào của hàm test, Pytest sẽ tự động chạy fixture đó và truyền kết quả vào.
import pytest
@pytest.fixture
def sample_user_data():
"""Fixture chuẩn bị dữ liệu mẫu cho user."""
return {"username": "mrseadev", "role": "admin"}
def test_user_role(sample_user_data):
# Pytest tự động inject fixture sample_user_data vào đây
assert sample_user_data["role"] == "admin"
2. Cơ chế yield cho Teardown (Dọn dẹp tài nguyên)
Trong Pytest, để thực hiện các thao tác dọn dẹp sau khi test chạy xong (như đóng kết nối database, xóa file tạm), bạn dùng từ khóa yield thay vì return:
- Code trước yield: Chạy trước hàm test (Setup).
- Code sau yield: Chạy sau khi hàm test kết thúc, bất kể test pass hay fail (Teardown).
@pytest.fixture
def database_connection():
print("\n[Setup] -> Mở kết nối Database...")
db = {"status": "connected"}
yield db # Trả đối tượng db cho hàm test sử dụng
print("\n[Teardown] -> Đóng kết nối Database an toàn!")
db.clear()
3. Tối ưu hiệu năng với scope (Phạm vi)
Mặc định, mọi fixture đều có scope="function" (nghĩa là cứ mỗi hàm test chạy là fixture được tạo mới và hủy đi). Tuy nhiên, nếu fixture tốn nhiều thời gian khởi tạo (như bật server giả lập hoặc kết nối database nặng), bạn có thể đổi scope:
- scope="module": Chạy 1 lần duy nhất cho cả file test.
- scope="session": Chạy 1 lần duy nhất cho toàn bộ quá trình chạy pytest của dự án.
@pytest.fixture(scope="module")
def expensive_server_setup():
print("\n--- Khởi động server test (Chạy 1 lần duy nhất cho module) ---")
server = "http://localhost:8080"
yield server
print("\n--- Tắt server test ---")
4. Tự động chạy với autouse=True
Nếu bạn có một fixture cần phải luôn luôn chạy trước mọi hàm test (như xóa cache, reset bảng ghi log) mà không muốn phải gõ tên nó làm tham số thủ công, hãy bật autouse=True:
@pytest.fixture(autouse=True)
def clean_cache_before_each_test():
# Tự động chạy trước mỗi hàm test trong phạm vi của nó
print("\n[Auto] Đang làm sạch cache hệ thống...")
🟨 PHẦN 2: PYTEST PARAMETRIZE (KIỂM THỬ HÀNG LOẠT)
Thay vì viết 5 hàm test riêng biệt chỉ để kiểm tra các trường hợp đầu vào/đầu ra khác nhau (login thành công, sai pass, thiếu tên, v.v.), decorator @pytest.mark.parametrize giúp bạn gom chúng lại trong một hàm test duy nhất.
1. Cú pháp chuỗi biến và danh sách Tuple
Bạn truyền vào một chuỗi chứa tên các biến phân tách bằng dấu phẩy, kèm theo một list các tuple chứa các bộ dữ liệu tương ứng:
import pytest
def check_login(username: str, pass_len: int) -> bool:
return len(username) > 3 and pass_len >= 6
# Parametrize kiểm thử nhiều bộ dữ liệu khác nhau cho 1 hàm test duy nhất
@pytest.mark.parametrize(
"username, pass_len, expected",
[
("admin", 8, True), # Case 1: Hợp lệ
("bob", 8, False), # Case 2: Username quá ngắn
("mrseadev", 4, False), # Case 3: Pass quá ngắn
]
)
def test_login_validation(username, pass_len, expected):
result = check_login(username, pass_len)
assert result == expected
2. Kết hợp Fixture và Parametrize thông qua request.param
Trong các kịch bản kiểm thử nâng cao, bạn có thể truyền dữ liệu được parametrize trực tiếp vào bên trong một Fixture thông qua biến đặc biệt request.param. Kỹ thuật này giúp bạn khởi tạo các môi trường động khác nhau cho các test case:
@pytest.fixture(params=[("db_sqlite", 3306), ("db_postgres", 5432)])
def database_environment(request):
# request.param sẽ lần lượt nhận từng tuple trong mảng params
db_type, port = request.param
print(f"\nKhởi tạo {db_type} tại cổng {port}")
env = {"type": db_type, "port": port}
yield env
print("Dọn dẹp môi trường {db_type}")
def test_database_connection(database_environment):
# Test sẽ tự động chạy 2 lần với 2 môi trường Database khác nhau!
assert database_environment["port"] in [3306, 5432]
📌 Lời Kết
Sự kết hợp hoàn hảo giữa Pytest Fixtures (quản lý ngữ cảnh và dọn dẹp tài nguyên linh hoạt) và Parametrize (kiểm thử hàng loạt dữ liệu gọn sạch) sẽ giúp bộ mã nguồn kiểm thử (Test Suite) của bạn vừa chuyên nghiệp, dễ bảo trì, vừa tiết kiệm tối đa thời gian thực thi. Hãy áp dụng ngay vào dự án Python tiếp theo của bạn!
Từ khóa tìm hiểu thêm: Pytest fixtures tutorial, Pytest parametrize multiple arguments, Pytest yield teardown, Pytest request param fixture.
Bình luận