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). 1. Khái niệm: Fixture là gì?
Trong Pytest, fixture là một hàm đặc biệt được dùng để:
-
Chuẩn bị môi trường trước khi test chạy (setup)
-
Giải phóng tài nguyên sau khi test xong (teardown)
-
Có thể tái sử dụng giữa nhiều test case.
#Cú pháp
import pytest
@pytest.fixture
def my_fixture():
print("Setup")
yield "some data"
print("Teardown")
#Khi dùng
def test_example(my_fixture):
print(my_fixture)
2. Fixture Playwright cơ bản
Ví dụ 1: Fixture page cơ bản (Bạn đang dùng mà không biết!)
Khi bạn viết def test_example(page):, page ở đây chính là một fixture do pytest-playwright cung cấp sẵn. Nó tự động làm 4 việc: (Setup) Mở trình duyệt, tạo context, tạo page MỚI ➝ (Yield) Trả về page cho bạn ➝ (Teardown) Đóng page, context, trình duyệt.
Ví dụ 2: Dùng sync_playwright để mở trình duyệt, tạo context và page:
import pytest
from playwright.sync_api import sync_playwright, expect
# --- Fixture định nghĩa ngay trong file ---
@pytest.fixture
def setup_driver():
print("\n=== Open browser ===")
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
print("\n--- Create new context ---")
context = browser.new_context()
print("\n>>> Open new page <<<")
page = context.new_page()
yield page # 👈 Trả page ra cho test sử dụng
print("\n=== Close driver ===")
page.close()
context.close()
browser.close()
# --- Test case dùng fixture ---
def test_google_search(setup_driver):
page = setup_driver
page.goto("https://www.google.com")
page.fill("textarea[name='q']", "Playwright Python")
page.keyboard.press("Enter")
expect(page).to_have_title_containing("Playwright Python")
# ✅ Chạy lệnh:
pytest -v test_sample.py
#🧾 Kết quả:
=== Open browser ===
--- Create new context ---
>>> Open new page <<<
=== Close driver ===
🔹 Ưu điểm: dễ hiểu, dễ thử nhanh.
🔹 Nhược điểm: chỉ dùng được trong file này.
3. Khái niệm: Scope là gì?
Scope (Phạm vi) quyết định tần suất một fixture được tạo ra (setup) và phá hủy (teardown). Nó là sự cân bằng giữa Tính Cô Lập (an toàn nhất) và Tốc Độ (nhanh nhất).
Hãy tưởng tượng bạn chuẩn bị dụng cụ cho đầu bếp (test case):
-
scope="function": Đưa cho mỗi đầu bếp một con dao mới tinh, dùng xong vứt đi. Rất an toàn, không bị lẫn mùi, nhưng tốn kém. -
scope="session": Đưa cho tất cả đầu bếp dùng chung một cái lò nướng đã bật sẵn từ sáng. Rất nhanh, không phải chờ lò nóng, nhưng nếu ai đó làm bẩn lò thì người sau bị ảnh hưởng.
4. Phân tích chi tiết 4 cấp độ Scope
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à vứt (teardown).
-
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"
-
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"
-
Tần suất: Chạy Setup 1 LẦN trước test đầu tiên trong file
.py, và Teardown 1 LẦN sau test cuối cùng trong 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ụ 1 (Kết nối DB): File
test_user_database.pychứa 50 test case cần truy vấn CSDL. Bạn tạo 1 fixturedb_connection(scope="module") để Mở kết nối CSDL 1 LẦN lúc bắt đầu file, và Đóng kết nối 1 LẦN lúc kết thúc file, thay vì mở/đóng 50 lần.
-
D. scope="session"
-
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ụ 1 (Đọc file Config): Tạo một fixture
config(scope="session") để đọc fileconfig.json(chứa URL, username, password) 1 LẦN duy nhất khi bắt đầu. Mọi test case, mọi file test sau đó đều có thể lấyconfignày ra dùng mà không cần đọc lại file. -
Ví dụ 2 (Khởi tạo Browser): (Nếu bạn tự build) Fixture
browsercủapytest-playwrightthực chất dùngscope="session". Nó khởi động trình duyệt (vd: Chromium) 1 LẦN duy nhất, và tất cả cácpage(scope="function") đều được "sinh ra" từ cáibrowserduy nhất đó. Khi tất cả test chạy xong, nó mới đóng trình duyệt. Đây là lý do Playwright chạy rất nhanh. -
Ví dụ 3 (Tạo Master Data): Tạo một "Super Admin" user (scope="session") 1 LẦN để dùng cho TẤT CẢ các test (chỉ dùng để login, không thay đổi).
📄test_scope_demo.pyimport 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")
▶️ Chạy lệnhpytest -v test_scope_demo.pyKết quả (rút gọn, để bạn dễ hiểu): [SETUP] session_fixture [SETUP] module_fixture [SETUP] function_fixture --- Test Case ONE --- [TEARDOWN] function_fixture [SETUP] function_fixture --- Test Case TWO --- [TEARDOWN] function_fixture [SETUP] class_fixture [SETUP] function_fixture >>> test_inside_class_1 running [TEARDOWN] function_fixture [SETUP] function_fixture >>> test_inside_class_2 running [TEARDOWN] function_fixture [TEARDOWN] class_fixture [TEARDOWN] module_fixture [TEARDOWN] session_fixture
-
Bảng tổng kết nhanh
| Scope | Mô tả | Khi nào chạy | Ví dụ minh họa |
|---|---|---|---|
function |
Mặc định | Trước và sau mỗi test | Reset dữ liệu test |
class |
Một lần cho mỗi class | Setup chung cho tất cả test trong 1 class | Đăng nhập 1 lần dùng chung |
module |
Một lần cho mỗi file test | Mở browser, context dùng lại | Tối ưu tốc độ |
session |
Một lần cho toàn bộ pytest run | Cấu hình chung toàn session | Tạo data test hoặc report global |
5. Chuyển fixture sang conftest.py để tái sử dụng
Khi bạn có nhiều file test, việc copy lại fixture là không cần thiết → ta tách ra riêng.
📁 Cấu trúc thư mục:
tests/
│
├── conftest.py # Chứa fixture chung
├── test_google.py # Gọi fixture
└── test_login.py # Gọi fixture khác
📄 conftest.py
import pytest
from playwright.sync_api import sync_playwright
@pytest.fixture(scope="session")
def browser_context():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
yield context
context.close()
browser.close()
@pytest.fixture
def page(browser_context):
page = browser_context.new_page()
yield page
page.close()
📄 test_google.py
from playwright.sync_api import expect
def test_google_search(page):
page.goto("https://www.google.com")
page.fill("textarea[name='q']", "Playwright Python")
page.keyboard.press("Enter")
expect(page).to_have_title_containing("Playwright Python")
📄 test_login.py
from playwright.sync_api import expect
def test_login(page):
page.goto("https://example.com/login")
page.fill("#username", "admin")
page.fill("#password", "123456")
page.click("button[type='submit']")
expect(page.locator("h1")).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.
