NỘI DUNG BÀI HỌC

Chống lặp code (DRY): Hiểu rõ Fixture là gì và tại sao nó là "cỗ máy" chuẩn bị (setup) và dọn dẹp (teardown) mạnh mẽ nhất của Pytest.
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:

📄 test_sample.py
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 page mặc định của pytest-playwright dù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_user tạ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ùng scope="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 fixture logged_in_page_class_scope (scope="class"). Nó sẽ Login 1 LẦN, sau đó cả 10 test case cùng chạy trên page đã đă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ùng page (hoặc self.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.py chứa 50 test case cần truy vấn CSDL. Bạn tạo 1 fixture db_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 file config.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ấy config nà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 browser của pytest-playwright thực chất dùng scope="session". Nó khởi động trình duyệt (vd: Chromium) 1 LẦN duy nhất, và tất cả các page (scope="function") đều được "sinh ra" từ cái browser duy 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.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")
      



      ▶️ Chạy lệnh

      pytest -v test_scope_demo.py
      
      Kế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 -v

💡 pytest sẽ tự động load fixture trong conftest.pykhông cần import thủ công.

 

Teacher

Teacher

Hà Lan

QA Automation

With over 5 years of experience in web, API, and mobile test automation, built strong expertise in designing and maintaining automation frameworks across various domains and international projects. Committed to mentoring and knowledge sharing, I provide practical guidance and proven techniques to help aspiring testers develop their skills and succeed in the automation field.

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học