NỘI DUNG BÀI HỌC
✅ Làm chủ Setup & Teardown: Sử dụng
yield để tách biệt phần "chuẩn bị" (ví dụ: đăng nhập, tạo data) khỏi phần "dọn dẹp" (ví dụ: đăng xuất, xóa data) một cách an toàn. ✅ TỐI ƯU HÓA TỐC ĐỘ: Hiểu sâu sắc 4 cấp độ
scope của Fixture (function, class, module, session) để quyết định khi nào cần cô lập (isolation) và khi nào cần hiệu suất (performance). Trong các framework cũ (như TestNG hay Unittest), chúng ta thường đau đầu với các hàm @BeforeMethod, @AfterMethod chồng chéo, copy-paste code khắp nơi. Pytest mang đến một "vũ khí" tối thượng mang tên Fixture. Nếu hiểu và làm chủ được nó, framework của bạn sẽ gọn gàng và chạy nhanh hơn gấp nhiều lần.
1. Khái niệm: Fixture là gì?
Đừng nghĩ về Fixture bằng những định nghĩa kỹ thuật khô khan. Hãy tưởng tượng việc chạy một Test Case giống như việc nấu một món ăn:
-
Setup (Chuẩn bị): Rửa chảo, bật bếp, thái thịt.
-
Test (Nấu): Bỏ đồ vào xào, nêm nếm.
-
Teardown (Dọn dẹp): Rửa bát, lau bếp.
Fixture chính là "Người phụ bếp" của bạn. Bạn chỉ cần ra lệnh: "Cho tôi cái chảo nóng". Người phụ bếp (Fixture) sẽ tự động làm bước Setup (rửa chảo, bật bếp), đưa chảo cho bạn nấu (Test), và ngay khi bạn nấu xong, họ sẽ tự động đem đi rửa (Teardown) mà bạn không cần phải nhắc.
Trong code, Fixture chia đôi hai thế giới Setup và Teardown chỉ bằng một từ khóa cực kỳ thanh lịch: yield.
#Cú pháp
import pytest
@pytest.fixture
def nguoi_phu_bep():
# 1. SETUP (Trước yield): Chạy TRƯỚC khi test bắt đầu
print("Bật bếp, lấy chảo")
chao = "Chảo đã nóng"
# 2. YIELD: Giao chảo cho hàm Test sử dụng
yield chao
# 3. TEARDOWN (Sau yield): Chạy SAU khi test kết thúc (Dù test pass hay fail)
print("Tắt bếp, đem chảo đi rửa")
# Hàm Test chỉ việc dùng "nguoi_phu_bep"
def test_chien_trung(nguoi_phu_bep):
chao_cua_toi = nguoi_phu_bep
print(f"Đang chiên trứng trên {chao_cua_toi}")
2. Scope (Phạm vi) - Chìa khóa tối ưu tốc độ thực thi
Scope quyết định việc "Người phụ bếp" sẽ chuẩn bị đồ cho bạn tần suất như thế nào. Chọn sai Scope, hệ thống chạy rùa bò; chọn đúng, bạn tối ưu được 80% thời gian chạy.
A. scope="function" (Mặc định)
-
Tần suất: Chạy lại Setup/Teardown cho MỖI HÀM TEST.
-
Analogy (Đời sống): Một chiếc bát ăn cơm dùng một lần. Mỗi "lần ăn" (test case) bạn lại lấy một cái bát mới tinh. Ăn xong là rửa (teardown).
- Bản chất: Đảm bảo test case này không bị "nhiễm khuẩn" (cookie, cache, data) từ test case trước.
-
Khi nào dùng trong Auto Test:
-
Đây là lựa chọn AN TOÀN và PHỔ BIẾN NHẤT.
-
Khi bạn cần sự cô lập tuyệt đối. Mỗi test case phải bắt đầu từ một trạng thái sạch sẽ, không bị ảnh hưởng bởi test case trước đó.
-
Ví dụ 1 (Playwright): Fixture
pagemặc định củapytest-playwrightdùng scope này. Mỗi test case (def test_abc(page):) nhận được một tab (page) hoàn toàn mới, không có cookie, không cache, không lịch sử từ test trước. -
Ví dụ 2 (Tạo Data): Một fixture
create_new_usertạo ra một user với email ngẫu nhiên trong CSDL. Bạn muốn mỗi test đăng ký (test_register_success,test_register_duplicate_email) đều chạy với một user mới hoàn toàn, nên bạn dùngscope="function".
-
B. scope="class" - Nhóm họp chung bàn
-
Tần suất: Chạy Setup 1 LẦN trước test method đầu tiên trong Class, và Teardown 1 LẦN sau test method cuối cùng trong Class.
-
Analogy (Đời sống): Một bàn làm việc nhóm. Cả nhóm (class) vào phòng họp (setup) lúc 9h sáng. Mọi người (test methods) dùng chung cái bàn đó để làm việc. 5h chiều họp xong, cả nhóm dọn dẹp bàn và rời đi (teardown).
-
Khi nào dùng trong Auto Test:
-
Khi bạn có một nhóm test (trong 1 class) đều yêu cầu chung một bước setup tốn kém và các test đó không phá hỏng trạng thái của nhau.
-
Ví dụ 1 (Login 1 lần): Bạn có
class TestProfileSettings:chứa 10 test nhỏ (test_update_avatar,test_update_bio,test_change_address...). Tất cả 10 test này đều yêu cầu user phải đăng nhập.-
Dùng
scope="function": Sẽ phải Login 10 lần và Logout 10 lần ➝ Rất chậm! -
Dùng
scope="class": Tạo 1 fixturelogged_in_page_class_scope(scope="class"). Nó sẽ Login 1 LẦN, sau đó cả 10 test case cùng chạy trênpageđã đăng nhập đó. Cuối cùng, nó Logout 1 LẦN. ➝ Nhanh hơn rất nhiều. -
Lưu ý: Để dùng, bạn phải đánh dấu class với
@pytest.mark.usefixtures("<tên_fixture>"). Các method trong class sẽ không nhận fixture làm tham số trực tiếp, mà sẽ dùngpage(hoặcself.page) đã được fixture đó "biến đổi"
-
-
C. scope="module" - Cục phát Wi-Fi của file
-
Tần suất: Chạy 1 lần cho toàn bộ file
.py. -
Analogy (Đời sống): Kết nối Wi-Fi cho một tầng lầu. Khi người đầu tiên của tầng lầu (module) đến, bộ phát Wi-Fi (fixture) được bật lên (setup). Mọi người trong tầng (all functions/classes in the file) dùng chung Wi-Fi đó. Khi người cuối cùng rời đi, bộ phát được tắt (teardown).
-
Khi nào dùng trong Auto Test:
-
Khi bạn cần một tài nguyên cho toàn bộ file test, nhưng không muốn ảnh hưởng đến các file test khác.
-
Ví dụ thực chiến: File test cần truy vấn Database 20 lần để verify data. Bạn tạo Fixture mở Connection DB (
scope="module"). Nó kết nối 1 lần lúc bắt đầu chạy file và đóng connection khi toàn bộ file kết thúc.
-
D. scope="session" - Móng nhà của Framework
-
Tần suất: Chạy Setup 1 LẦN DUY NHẤT khi bắt đầu toàn bộ phiên chạy
pytest, và Teardown 1 LẦN DUY NHẤT khi tất cả test trong mọi file đã chạy xong. -
Analogy (Đời sống): Xây dựng tòa nhà. Bạn xây tòa nhà (fixture) 1 LẦN (setup). Sau đó, hàng ngàn người (test cases) ra vào, sử dụng tòa nhà đó trong nhiều năm. Hàng chục năm sau, khi tòa nhà không còn được sử dụng nữa, nó mới bị phá dỡ (teardown).
-
Khi nào dùng trong Auto Test:
-
Cho những tài nguyên siêu tốn kém để tạo ra, và thường là chỉ đọc (read-only), hoặc được dùng chung bởi tất cả các test.
-
Ví dụ : Đọc file
config.json(chứa URL môi trường Staging/Live, account test). 2. Khởi động tiến trình (process) Chromium vật lý. Bật trình duyệt tốn vài giây, nên ta chỉ bật 1 lần (scope session), sau đó các test case (scope function) chỉ việc mượn nó để tạo tab (context).
📄test_scope_demo.py -
import pytest from playwright.sync_api import sync_playwright # ================================================== # 🔹 FIXTURE 1: scope="function" # → Mỗi test case chạy 1 lần (mặc định) # ================================================== @pytest.fixture(scope="function") def function_fixture(): print("\n[SETUP] function_fixture") yield "function_scope" print("[TEARDOWN] function_fixture") # ================================================== # 🔹 FIXTURE 2: scope="class" # → Chạy 1 lần cho mỗi class test # ================================================== @pytest.fixture(scope="class") def class_fixture(): print("\n[SETUP] class_fixture") yield "class_scope" print("[TEARDOWN] class_fixture") # ================================================== # 🔹 FIXTURE 3: scope="module" # → Chạy 1 lần cho mỗi file (module) # ================================================== @pytest.fixture(scope="module") def module_fixture(): print("\n[SETUP] module_fixture") with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() yield context print("[TEARDOWN] module_fixture") context.close() browser.close() # ================================================== # 🔹 FIXTURE 4: scope="session" # → Chạy 1 lần duy nhất cho toàn bộ session pytest # ================================================== @pytest.fixture(scope="session") def session_fixture(): print("\n[SETUP] session_fixture") yield "session_scope" print("[TEARDOWN] session_fixture") # ================================================== # 🔹 Test case sử dụng các fixture khác nhau # ================================================== def test_case_one(module_fixture, function_fixture, session_fixture): print("\n--- Test Case ONE ---") page = module_fixture.new_page() page.goto("https://example.com") print("Page title:", page.title()) page.close() def test_case_two(module_fixture, function_fixture): print("\n--- Test Case TWO ---") page = module_fixture.new_page() page.goto("https://playwright.dev") print("Page title:", page.title()) page.close() # ================================================== # 🔹 Test class để minh họa scope="class" # ================================================== @pytest.mark.usefixtures("class_fixture", "module_fixture") class TestExample: def test_inside_class_1(self, function_fixture): print("\n>>> test_inside_class_1 running") def test_inside_class_2(self, function_fixture): print("\n>>> test_inside_class_2 running")
-
Bảng tổng kết nhanh:
| Tiêu chí so sánh | scope="function" | scope="class" | scope="module" | scope="session" |
| Tần suất chạy Setup | Trước mỗi hàm test | Trước mỗi class test | Trước mỗi file .py |
1 lần cho cả đợt test |
| Mức độ an toàn dữ liệu | Tối đa (Cô lập hoàn toàn) | Trung bình | Thấp | Rất thấp (Dễ bị ảnh hưởng lẫn nhau) |
| Tốc độ thực thi | Chậm hơn | Nhanh | Rất nhanh | Tối ưu tốc độ cao nhất |
| Ví dụ thực tế | Mở một tab ẩn danh mới tinh để test tính năng | Đăng nhập một lần để chạy toàn bộ test trong class | Mở kết nối đến Database | Khởi động trình duyệt Chromium/Đọc file config |
3. Tổ chức chuyên nghiệp với conftest.py
Khi dự án automation test phát triển lên hàng trăm file, việc khai báo đi khai báo lại các fixture ở từng file sẽ vi phạm nguyên tắc DRY. Pytest cung cấp một file đặc biệt tên là conftest.py.
Khi bạn đặt các hàm fixture vào trong file conftest.py, Pytest sẽ tự động quét, nhận diện và cho phép tất cả các file test trong cùng thư mục (hoặc thư mục con) gọi trực tiếp các fixture này bằng tên tham số mà không cần sử dụng lệnh import.
📁 Cấu trúc thư mục:
project/
│
├── conftest.py # Fixtures dùng chung cho toàn bộ project
│
├── pages/
│ ├── base_page.py
│ └── login_page.py
│
└── tests/
├── conftest.py # Fixtures dành riêng cho các test trong thư mục tests
└── test_login_pom.py
📄 conftest.py
# conftest.py
import pytest
from playwright.sync_api import sync_playwright
@pytest.fixture(scope="session")
def browser_instance():
p = sync_playwright().start()
browser = p.chromium.launch(headless=False)
yield browser
browser.close()
p.stop()
@pytest.fixture(scope="function")
def page(browser_instance):
context = browser_instance.new_context()
page = context.new_page()
yield page
page.close()
context.close()
📄 test_login_pom.py
from playwright.sync_api import expect
def test_login(page):
page.goto("https://example.com/login")
username_input = page.locator("#username")
password_input = page.locator("#password")
submit_button = page.locator("button[type='submit']")
heading = page.locator("h1")
username_input.fill("admin")
password_input.fill("123456")
submit_button.click()
expect(heading).to_have_text("Dashboard")
✅ Chạy tất cả test:
💡 pytest sẽ tự động load fixture trong conftest.py mà không cần import thủ công.
4. Điểm mạnh "vô đối" và Mức độ sử dụng trong Automation
Trong thực tế xây dựng Framework, 100% các dự án dùng Pytest đều phải dùng Fixture. Nó là nền móng vì mang lại 3 sức mạnh cốt lõi:
-
Tự động dọn dẹp (Safety First): Không sợ quên đóng trình duyệt (gây tốn RAM) hay quên xóa data rác trong Database. Dù Test Case có bị lỗi (Exception) ngay giữa chừng, đoạn code bên dưới
yield(Teardown) vẫn chắc chắn được gọi. -
Chia sẻ không cần Import (Dependency Injection): Đặt Fixture vào file
conftest.py, toàn bộ hàng trăm file test của bạn có thể gọi tên Fixture đó ra dùng ngay lập tức mà không cần 1 dòngimportnào. -
Module hóa cao độ (Tái sử dụng): Một Fixture có thể gọi một Fixture khác. Bạn có thể xây dựng các block logic nhỏ rồi ghép chúng lại với nhau rất mượt mà.
