最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

使用Playwright+Pytest構(gòu)建Web?UI自動(dòng)化測(cè)試框架完整示例

 更新時(shí)間:2026年05月03日 11:54:50   作者:imzed  
Playwright與PyTest強(qiáng)強(qiáng)聯(lián)合,打造高效Web自動(dòng)化測(cè)試方案,前者提供現(xiàn)代化跨瀏覽器自動(dòng)化能力,后者帶來結(jié)構(gòu)化測(cè)試管理與強(qiáng)大擴(kuò)展性,這篇文章主要介紹了使用Playwright+Pytest構(gòu)建Web?UI自動(dòng)化測(cè)試框架的相關(guān)資料,需要的朋友可以參考下

一、為什么選擇 Playwright?

1.1 Playwright vs Selenium 對(duì)比

先來看一個(gè)直觀的對(duì)比:

特性PlaywrightSelenium
瀏覽器支持? 原生支持 Chromium、Firefox、WebKit?? 需要額外驅(qū)動(dòng)程序
等待機(jī)制? 內(nèi)置自動(dòng)同步,無需顯式等待? 需要手動(dòng) WebDriverWait
網(wǎng)絡(luò)攔截? 原生 Mock API 能力? 第三方擴(kuò)展支持差
鏈路追蹤? 內(nèi)置 Trace 功能,完整記錄操作路徑? 不支持
跨域處理? 無跨域限制?? 有跨域問題
執(zhí)行效率? 更快(優(yōu)化程度更高)?? 相對(duì)較慢
API 設(shè)計(jì)? 現(xiàn)代化、簡潔直觀?? 有歷史包袱

核心優(yōu)勢(shì)示例:

# ? Playwright:自動(dòng)等待,開箱即用
page.click('button')  # 自動(dòng)等待元素可點(diǎn)擊,無需手動(dòng)等待
page.fill('#username', 'admin')  # 自動(dòng)等待輸入框可用

# ? Selenium:需要手動(dòng)配置等待
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.TAG_NAME, "button")))
element.click()

二、架構(gòu)設(shè)計(jì)與分層

2.1 項(xiàng)目目錄結(jié)構(gòu)

playwright_ui/
├── cases/                      # ?? 測(cè)試用例層
│   ├── conftest.py            # Fixture 定義
│   ├── test_login.py          # 登錄功能測(cè)試
│   ├── test_add_project.py    # 創(chuàng)建項(xiàng)目測(cè)試
│   ├── test_project_list.py   # 項(xiàng)目列表測(cè)試
│   └── more_accounts/         # 多賬號(hào)場(chǎng)景
│
├── pages/                      # ?? Page Object Model 層
│   ├── login_page.py          # 登錄頁面對(duì)象
│   ├── register_page.py       # 注冊(cè)頁面對(duì)象
│   ├── add_project_page.py    # 新增項(xiàng)目頁面對(duì)象
│   ├── project_list_page.py   # 項(xiàng)目列表頁面對(duì)象
│   └── list_env_page.py       # 環(huán)境列表頁面對(duì)象
│
├── plugins/                    # ?? 自定義插件
│   ├── pytest_playwright.py   # Playwright 集成插件
│   └── pytest_base_url_plugin.py  # URL 管理插件
│
├── mocks/                      # ?? Mock 數(shù)據(jù)
│   └── mock_api.py            # API Mock 配置
│
├── conftest.py                # 全局 Fixture
├── pytest.ini                 # Pytest 配置
├── run.py                     # 啟動(dòng)入口
└── requirements.txt           # 依賴列表

2.2 分層架構(gòu)設(shè)計(jì)

┌──────────────────────────────────┐
│   Test Case Layer                │
│   - 業(yè)務(wù)場(chǎng)景測(cè)試                  │
│   - 數(shù)據(jù)驅(qū)動(dòng)                      │
└─────────────┬──────────────────┘
              │
┌─────────────▼──────────────────┐
│   Page Object Model Layer      │
│   - 頁面元素封裝                │
│   - 業(yè)務(wù)操作方法                │
└─────────────┬──────────────────┘
              │
┌─────────────▼──────────────────┐
│   Fixture & Plugin Layer       │
│   - Browser/Page fixture       │
│   - Screenshot/Video/Trace     │
│   - 自動(dòng)化報(bào)告                  │
└─────────────┬──────────────────┘
              │
┌─────────────▼──────────────────┐
│   Playwright API               │
│   - 瀏覽器自動(dòng)化能力             │
└──────────────────────────────────┘

三、核心特性詳解

3.1 Page Object Model(POM)模式

什么是 POM? 將頁面元素定位和業(yè)務(wù)操作封裝為對(duì)象,提高代碼可維護(hù)性。

示例:LoginPage

# pages/login_page.py
from playwright.sync_api import Page

class LoginPage:
    """登錄頁面對(duì)象"""
    
    def __init__(self, page: Page):
        self.page = page
        # 元素定位 - 使用 Playwright 的強(qiáng)大選擇器
        self.locator_username = page.get_by_label("用 戶 名:")
        self.locator_password = page.get_by_label("密     碼:")
        self.locator_login_btn = page.locator('text=立即登錄')
        self.locator_login_error = page.locator('text=賬號(hào)或密碼不正確!')
    
    # 頁面操作 - 原子操作
    def navigate(self):
        """導(dǎo)航到登錄頁"""
        self.page.goto("/login.html")
    
    def fill_username(self, username):
        """輸入用戶名"""
        self.locator_username.fill(username)
    
    def fill_password(self, password):
        """輸入密碼"""
        self.locator_password.fill(password)
    
    def click_login_button(self):
        """點(diǎn)擊登錄按鈕"""
        self.locator_login_btn.click()
    
    # 業(yè)務(wù)操作 - 組合操作
    def login(self, username, password) -> None:
        """完整登錄流程"""
        self.fill_username(username)
        self.fill_password(password)
        self.click_login_button()

測(cè)試代碼變得簡潔優(yōu)雅:

# cases/test_login.py
class TestLogin:
    """登錄功能測(cè)試"""
    
    @pytest.fixture(autouse=True)
    def start_for_each(self, unlogin_page):
        self.login = LoginPage(unlogin_page)
        self.login.navigate()
        yield
    
    def test_login_success(self):
        """正常登錄流程"""
        # 業(yè)務(wù)代碼一目了然
        self.login.login("py", "123456")
        expect(self.login.page).to_have_url("/index.html")
    
    def test_login_with_empty_username(self):
        """用戶名為空驗(yàn)證"""
        self.login.fill_username('')
        self.login.fill_password('123456')
        self.login.click_login_button()
        
        # 使用 Playwright 的斷言
        expect(self.login.locator_username_tip1).to_be_visible()
        expect(self.login.locator_username_tip1).to_contain_text("不能為空")

POM 的優(yōu)勢(shì):

? 提高可維護(hù)性:定位器改變只需改一處
? 提高復(fù)用性:多個(gè)測(cè)試文件可共享同一 Page 對(duì)象
? 提高可讀性:測(cè)試用例自文檔化
? 便于擴(kuò)展:添加新功能無需修改測(cè)試代碼

3.2 強(qiáng)大的 Fixture 機(jī)制

Pytest 的 Fixture 是依賴注入的優(yōu)雅實(shí)現(xiàn),框架采用三層 Fixture 架構(gòu):

# ┌─────────────────────────────────────────┐
# │ Session 級(jí)別(整個(gè)測(cè)試會(huì)話)              │
# │ - browser_type_launch_args              │
# │ - browser_context_args                  │
# │ - login_first (全局登錄)                │
# └─────────────────────────────────────────┘
#
# ┌─────────────────────────────────────────┐
# │ Module 級(jí)別(每個(gè)測(cè)試模塊)              │
# │ - unlogin_context (無登錄上下文)         │
# │ - admin_context (管理員上下文)          │
# └─────────────────────────────────────────┘
#
# ┌─────────────────────────────────────────┐
# │ Function 級(jí)別(每個(gè)測(cè)試函數(shù))            │
# │ - page (頁面對(duì)象)                       │
# │ - unlogin_page (無登錄頁面)             │
# └─────────────────────────────────────────┘

全局登錄 Fixture(Session 級(jí)別):

# cases/conftest.py
@pytest.fixture(scope="session", autouse=True)
def login_first(context, base_url, pytestconfig) -> None:
    """
    有些網(wǎng)站網(wǎng)頁關(guān)閉cookie就失效了,全局登錄一次
    這樣所有測(cè)試都能共享登錄狀態(tài),避免重復(fù)登錄浪費(fèi)時(shí)間
    """
    print("base_url----", base_url)
    page = context.new_page()
    LoginPage(page).navigate()
    LoginPage(page).login("py", "123456")
    # 等待登錄成功頁面重定向
    page.wait_for_url(url='**/index.html')

多賬號(hào)隔離 Fixture(Module 級(jí)別):

# cases/more_accounts/conftest.py
@pytest.fixture(scope="module")
def admin_context(browser, base_url, pytestconfig, browser_context_args):
    """
    創(chuàng)建 admin 賬戶的獨(dú)立上下文
    避免與默認(rèn)登錄沖突,支持多賬號(hào)測(cè)試
    """
    context = browser.new_context(**browser_context_args)
    page = context.new_page()
    LoginPage(page).navigate()
    LoginPage(page).login("admin", "123456")
    page.wait_for_url(url='**/index.html')
    yield context
    context.close()

使用示例:

class TestAddProject:
    """添加項(xiàng)目 - 使用全局登錄"""
    
    @pytest.fixture(autouse=True)
    def start_for_each(self, login_first, page):
        self.add_project = AddProjectPage(page)
        self.add_project.navigate()
        yield

class TestLogin:
    """登錄測(cè)試 - 使用無登錄上下文"""
    
    @pytest.fixture(autouse=True)
    def start_for_each(self, unlogin_page):
        self.login = LoginPage(unlogin_page)
        self.login.navigate()
        yield

3.3 網(wǎng)絡(luò)攔截與 API Mock

Playwright 的網(wǎng)絡(luò)攔截能力是其一大優(yōu)勢(shì),可以輕松 Mock API 返回值,測(cè)試異常場(chǎng)景。

API Mock 配置:

# mocks/mock_api.py
mock_project_400 = (
    '**/api/addProject',
    lambda route: route.abort_with_error_code(400)
)

mock_project_500 = (
    '**/api/addProject',
    lambda route: route.abort_with_error_code(500)
)

mock_project_200 = (
    '**/api/addProject',
    lambda route: route.continue_()
)

測(cè)試不同 HTTP 狀態(tài)碼:

# cases/test_add_project.py
def test_add_project_400(self, page):
    """項(xiàng)目名稱重復(fù),返回 400 錯(cuò)誤"""
    self.add_project.fill_project_name("yo yo")
    self.add_project.fill_publish_app("xx")
    
    # Mock API 返回 400
    page.route(**mock_api.mock_project_400)
    self.add_project.click_save_button()
    
    # 驗(yàn)證 UI 正確處理錯(cuò)誤
    expect(self.add_project.locator_boot_box).to_be_visible()
    expect(self.add_project.locator_boot_box).to_contain_text('已存在')

def test_add_project_500(self, page):
    """服務(wù)器異常,返回 500 錯(cuò)誤"""
    self.add_project.fill_project_name("test")
    
    # Mock API 返回 500
    page.route(**mock_api.mock_project_500)
    self.add_project.click_save_button()
    
    # 驗(yàn)證 UI 提示
    expect(self.add_project.locator_boot_box).to_contain_text('操作異常')

捕獲網(wǎng)絡(luò)請(qǐng)求和響應(yīng):

def test_login_ajax(self):
    """捕獲并驗(yàn)證登錄請(qǐng)求"""
    self.login.fill_username("py")
    self.login.fill_password("123456")
    
    # 捕獲 API 請(qǐng)求
    with self.login.page.expect_request('**/api/login') as req:
        self.login.click_login_button()
    
    # 驗(yàn)證請(qǐng)求內(nèi)容
    assert req.value.method == 'POST'
    assert req.value.header_value('content-type') == 'application/json'
    assert req.value.post_data_json == {
        'username': 'py',
        'password': '123456'
    }

def test_login_ajax_response(self):
    """捕獲并驗(yàn)證登錄響應(yīng)"""
    self.login.fill_username("py")
    self.login.fill_password("123456")
    
    # 捕獲 API 響應(yīng)
    with self.login.page.expect_response('**/api/login') as res:
        self.login.click_login_button()
    
    # 驗(yàn)證響應(yīng)
    assert res.value.ok
    assert res.value.status == 200

3.4 項(xiàng)目列表搜索功能測(cè)試

這個(gè)例子展示了如何進(jìn)行實(shí)際的業(yè)務(wù)流程測(cè)試:

# cases/test_project_list.py
class TestProjectList:
    """項(xiàng)目列表功能測(cè)試"""
    
    @pytest.fixture(autouse=True)
    def start_for_each(self, login_first, page):
        """每個(gè)測(cè)試前打開項(xiàng)目列表頁"""
        self.project = ProjectListPage(page)
        self.project.navigate()
        yield
    
    def test_search_project_ajax(self):
        """驗(yàn)證搜索請(qǐng)求的參數(shù)和方法"""
        self.project.search_project("test")
        
        # 點(diǎn)擊搜索時(shí)捕獲請(qǐng)求
        with self.project.page.expect_request('**/api/project**') as req:
            self.project.click_search_button()
        
        # 驗(yàn)證請(qǐng)求參數(shù)
        assert "project_name=test" in req.value.url
        assert req.value.method == "GET"
    
    def test_search_project_empty_result(self):
        """測(cè)試搜索無結(jié)果的情況"""
        self.project.search_project("test")
        
        # Mock 搜索返回空結(jié)果
        self.project.page.route(**mock_api.mock_project_search_0)
        self.project.click_search_button()
        
        # 驗(yàn)證提示信息
        expect(self.project.locator_table_tr).to_contain_text('沒有找到匹配的記錄')
    
    def test_search_project_single_result(self):
        """測(cè)試搜索返回單條結(jié)果"""
        self.project.search_project("test")
        
        # Mock 搜索返回 1 條結(jié)果
        self.project.page.route(**mock_api.mock_project_search_1)
        self.project.click_search_button()
        
        # 驗(yàn)證結(jié)果數(shù)量
        expect(self.project.locator_table_tr).to_have_count(1)
    
    def test_table_delete(self):
        """表格行內(nèi)刪除操作"""
        # Mock 搜索結(jié)果
        self.project.page.route(**mock_api.mock_project_search_1)
        self.project.page.reload()
        
        # 點(diǎn)擊刪除
        self.project.locator_table_delete.click()
        expect(self.project.locator_boot_box).to_contain_text('確定要?jiǎng)h除選中的數(shù)據(jù)?')
        
        # Mock 刪除返回 403(無權(quán)限)
        self.project.page.route(**mock_api.mock_project_delete_403)
        self.project.locator_boot_box_accept.click()
        
        # 驗(yàn)證錯(cuò)誤提示
        expect(self.project.locator_boot_box.last).to_contain_text('無權(quán)限操作')

3.5 參數(shù)化測(cè)試

使用 @pytest.mark.parametrize 進(jìn)行數(shù)據(jù)驅(qū)動(dòng)測(cè)試,避免代碼重復(fù):

# cases/test_env_list.py
class TestEnvList:
    """環(huán)境列表參數(shù)化測(cè)試示例"""
    
    @pytest.mark.parametrize("name", ["abc!@", "$32", "\\xx"])
    def test_add_env_name_invalid(self, name):
        """
        參數(shù)化測(cè)試:不能有特殊字符
        生成 3 個(gè)測(cè)試用例
        """
        self.env.click_add_env()
        self.env.input_env_name(name)
        self.env.click_modal_save()
        
        # 驗(yàn)證錯(cuò)誤提示
        expect(self.env.locator_modal_env_tip3).to_be_visible()
        expect(self.env.locator_modal_env_tip3).to_have_text('不能有特殊字符')
    
    @pytest.mark.parametrize("address", ["abchttp", "httpx:", "httpsx://"])
    def test_add_env_address_invalid(self, address):
        """
        參數(shù)化測(cè)試:URL 格式驗(yàn)證
        生成 3 個(gè)測(cè)試用例
        """
        self.env.click_add_env()
        self.env.input_env_name('env')
        self.env.input_env_address(address)
        self.env.click_modal_save()
        
        # 驗(yàn)證錯(cuò)誤提示
        expect(self.env.locator_boot_box).to_be_visible()
        expect(self.env.locator_boot_box).to_have_text('base_url must start with http:// or https://')

執(zhí)行結(jié)果:

$ pytest cases/test_env_list.py::TestEnvList::test_add_env_name_invalid -v

test_add_env_name_invalid[abc!@] PASSED
test_add_env_name_invalid[$32] PASSED
test_add_env_name_invalid[\xx] PASSED

3.6 自動(dòng)化報(bào)告生成

框架集成了完整的自動(dòng)化報(bào)告系統(tǒng),??括截圖、視頻、鏈路追蹤:

# plugins/pytest_playwright.py 中實(shí)現(xiàn)
@pytest.fixture
def page(context, pytestconfig, request):
    page = context.new_page()
    yield page
    
    # 判斷測(cè)試是否失敗
    failed = request.node.rep_call.failed if hasattr(request.node, "rep_call") else True
    
    # 根據(jù)配置自動(dòng)截圖
    screenshot_option = pytestconfig.getoption("--screenshot")
    capture_screenshot = screenshot_option == "on" or (failed and screenshot_option == "only-on-failure")
    
    if capture_screenshot:
        for index, page in enumerate(pages):
            screenshot_path = _build_artifact_test_folder(pytestconfig, request, f"test-{human_readable_status}-{index + 1}.png")
            page.screenshot(timeout=5000, path=screenshot_path)
            # 自動(dòng)附加到 Allure 報(bào)告
            allure.attach.file(screenshot_path, attachment_type=allure.attachment_type.PNG)
    
    # 自動(dòng)錄制視頻
    video_option = pytestconfig.getoption("--video")
    preserve_video = video_option == "on" or (failed and video_option == "retain-on-failure")
    
    if preserve_video:
        for page in pages:
            video = page.video
            if video:
                video_path = video.path()
                video.save_as(path=file_path)
                allure.attach.file(file_path, attachment_type=allure.attachment_type.WEBM)

啟動(dòng)報(bào)告:

# 運(yùn)行測(cè)試
python run.py

# 或使用命令行
pytest --alluredir ./reports
allure serve ./reports

報(bào)告產(chǎn)物:

test-results/
├── trace.zip           # 完整鏈路追蹤(可在線查看)
├── test-failed-1.png   # 失敗時(shí)截圖
└── test-failed.webm    # 失敗時(shí)視頻

四、最佳實(shí)踐

4.1 等待策略

Playwright 的智能等待是其一大優(yōu)勢(shì),但也需要正確使用:

# ? 優(yōu)先使用隱式等待(推薦)
page.click()  # 自動(dòng)等待元素可點(diǎn)擊
page.fill()   # 自動(dòng)等待元素可輸入

# ? 導(dǎo)航等待
with page.expect_navigation(url='**/list_project.html'):
    self.add_project.click_save_button()

# ? 網(wǎng)絡(luò)等待
page.wait_for_load_state('networkidle')  # 所有網(wǎng)絡(luò)請(qǐng)求完成

# ? 避免硬編碼等待
time.sleep(2)  # 不要這樣做!

4.2 測(cè)試組織最佳實(shí)踐

class TestLogin:
    """登錄功能測(cè)試"""
    
    @pytest.fixture(autouse=True)
    def setup(self, unlogin_page):
        """前置條件:準(zhǔn)備測(cè)試環(huán)境"""
        self.login = LoginPage(unlogin_page)
        self.login.navigate()
        yield
        # 后置處理(可選)
    
    # 正常流程
    def test_login_success(self):
        """用戶使用正確的用戶名和密碼能成功登錄"""
        self.login.login("py", "123456")
        expect(self.login.page).to_have_url("/index.html")
    
    # 異常場(chǎng)景1:輸入驗(yàn)證
    def test_login_with_empty_username(self):
        """用戶名為空時(shí)應(yīng)該顯示錯(cuò)誤提示"""
        self.login.fill_username('')
        self.login.fill_password('123456')
        self.login.click_login_button()
        expect(self.login.locator_username_tip1).to_contain_text("不能為空")
    
    # 異常場(chǎng)景2:錯(cuò)誤憑證
    def test_login_with_wrong_password(self):
        """密碼錯(cuò)誤時(shí)應(yīng)該顯示錯(cuò)誤提示"""
        self.login.login("py", "wrongpassword")
        expect(self.login.locator_login_error).to_be_visible()
    
    # 邊界值測(cè)試
    def test_login_with_long_username(self):
        """用戶名過長時(shí)應(yīng)該受限"""
        self.login.fill_username('hello world hello world hello world')
        expect(self.login.locator_login_btn).not_to_be_enabled()

4.3 調(diào)試技巧

# 1. 使用 --headed 模式查看瀏覽器窗口
pytest --headed cases/test_login.py

# 2. 使用 --slowmo 延遲每個(gè)操作
pytest --slowmo=1000 cases/test_login.py

# 3. 啟用詳細(xì) Trace
pytest --tracing=on cases/test_login.py

# 4. 查看 Trace
playwright show-trace test-results/trace.zip

# 5. 在代碼中暫停
def test_with_pause(page):
    page.pause()  # 瀏覽器會(huì)暫停,允許手動(dòng)操作

五、性能優(yōu)化建議

5.1 減少重復(fù)登錄

# ? 不好的做法:每個(gè)測(cè)試都登錄
@pytest.fixture(autouse=True)
def setup(self, page):
    LoginPage(page).login("py", "123456")  # 浪費(fèi)時(shí)間

# ? 好的做法:全局登錄一次
@pytest.fixture(scope="session", autouse=True)
def login_first(context):
    page = context.new_page()
    LoginPage(page).login("py", "123456")
    # 所有測(cè)試共享 Cookie

5.2 使用 Mock API

# ? 真實(shí) API 調(diào)用(慢)
self.add_project.click_save()  # 等待數(shù)據(jù)庫操作

# ? Mock API(快)
page.route('**/api/addProject', lambda route: route.continue_())

5.3 并行執(zhí)行

# 安裝 pytest-xdist
pip install pytest-xdist

# 4 個(gè)進(jìn)程并行執(zhí)行
pytest -n 4

# 自動(dòng)檢測(cè) CPU 核數(shù)
pytest -n auto

六、完整示例:添加項(xiàng)目功能測(cè)試

最后,讓我展示一個(gè)完整的功能測(cè)試示例:

# cases/test_add_project.py
from pages.add_project_page import AddProjectPage
from playwright.sync_api import expect, Page
import pytest
import uuid
from mocks import mock_api

class TestAddProject:
    """添加項(xiàng)目功能測(cè)試"""
    
    @pytest.fixture(autouse=True)
    def start_for_each(self, login_first, page: Page):
        """前置條件:打開添加項(xiàng)目頁"""
        print("for each--start: 打開添加項(xiàng)目頁")
        self.add_project = AddProjectPage(page)
        self.add_project.navigate()
        yield
        print("for each--end: 后置操作")
    
    # 異常場(chǎng)景:輸入驗(yàn)證
    @pytest.mark.parametrize("name, app, desc", [
        ["abc!@", "", ""],              # 特殊字符
        ["aaaaabbbbbcccccdddddeeeeefffff1", "", ""],  # 超過 30 字符
        ["abc", "aa!@", ""]             # app 特殊字符
    ])
    def test_add_project_disabled(self, name, app, desc):
        """異常場(chǎng)景-項(xiàng)目名稱無效:特殊字符/大于30個(gè)字符"""
        self.add_project.fill_project_name(name)
        self.add_project.fill_publish_app(app)
        self.add_project.fill_project_desc(desc)
        # 斷言提交按鈕不可點(diǎn)擊
        expect(self.add_project.locator_save_button).to_be_disabled()
    
    # 異常場(chǎng)景:空值驗(yàn)證
    def test_add_project_null(self):
        """異常場(chǎng)景-項(xiàng)目名稱不能為空"""
        self.add_project.fill_project_name("")
        # 斷言提交按鈕不可點(diǎn)擊
        expect(self.add_project.locator_save_button).to_be_disabled()
    
    # 異常場(chǎng)景:服務(wù)器返回 400(項(xiàng)目已存在)
    def test_add_project_400(self, page: Page):
        """項(xiàng)目名稱重復(fù),服務(wù)器返回 400 錯(cuò)誤"""
        self.add_project.fill_project_name("yo yo")
        self.add_project.fill_publish_app("xx")
        self.add_project.fill_project_desc("xxx")
        
        # Mock API 返回 400
        page.route(**mock_api.mock_project_400)
        self.add_project.click_save_button()
        
        # 驗(yàn)證錯(cuò)誤提示
        expect(self.add_project.locator_boot_box).to_be_visible()
        expect(self.add_project.locator_boot_box).to_contain_text('已存在')
    
    # 異常場(chǎng)景:服務(wù)器返回 500(服務(wù)器異常)
    def test_add_project_500(self, page: Page):
        """服務(wù)器異常,返回 500 狀態(tài)碼"""
        self.add_project.fill_project_name("test")
        self.add_project.fill_publish_app("xx")
        self.add_project.fill_project_desc("xxx")
        
        # Mock API 返回 500
        page.route(**mock_api.mock_project_500)
        self.add_project.click_save_button()
        
        # 驗(yàn)證錯(cuò)誤提示
        expect(self.add_project.locator_boot_box).to_contain_text('操作異常')
    
    # 正常流程:成功添加項(xiàng)目
    def test_add_project_success(self, page: Page):
        """提交成功,跳轉(zhuǎn)到項(xiàng)目列表"""
        # 生成隨機(jī)項(xiàng)目名
        project_name = str(uuid.uuid4()).replace('-', '')[:25]
        
        self.add_project.fill_project_name(project_name)
        self.add_project.fill_publish_app("xx")
        self.add_project.fill_project_desc("xxx")
        
        # 驗(yàn)證頁面重定向到列表頁
        with page.expect_navigation(url="**/list_project.html"):
            self.add_project.click_save_button()
    
    # 完整流程:驗(yàn)證新項(xiàng)目在列表中
    def test_add_project_success_verify_in_list(self, page: Page):
        """添加項(xiàng)目成功后,驗(yàn)證項(xiàng)目??現(xiàn)在列表中"""
        # 生成隨機(jī)項(xiàng)目???
        new_project_name = str(uuid.uuid4()).replace('-', '')[:25]
        
        self.add_project.fill_project_name(new_project_name)
        self.add_project.fill_publish_app("xx")
        self.add_project.fill_project_desc("xxx")
        
        self.add_project.click_save_button()
        
        # 等待頁面重定向和網(wǎng)絡(luò)請(qǐng)求完成
        self.add_project.page.wait_for_load_state('networkidle')
        
        # 獲取表格中所有項(xiàng)目名稱
        loc_projects = self.add_project.page.locator('//table[@id="table"]//td[3]/a')
        project_names = [i.inner_text() for i in loc_projects.all()]
        
        print(f"新增項(xiàng)目名稱: {new_project_name}")
        print(f"列表中的項(xiàng)目: {project_names}")
        
        # 驗(yàn)證新項(xiàng)目在列表中
        assert new_project_name in project_names, f"項(xiàng)目 {new_project_name} 未在列表中"

七、總結(jié)與啟示

通過這個(gè)企業(yè)級(jí)的 Playwright UI 自動(dòng)化測(cè)試框架,我們學(xué)到了:

核心設(shè)計(jì)原則:

  1. 分層架構(gòu):清晰的分層使代碼更易維護(hù)和擴(kuò)展
  2. Page Object Model:將頁面元素和業(yè)務(wù)邏輯分離,提高復(fù)用性
  3. Fixture 機(jī)制:優(yōu)雅的依賴注入,減少代碼重復(fù)
  4. 網(wǎng)絡(luò)攔截:Mock API 能力,便于測(cè)試異常場(chǎng)景
  5. 自動(dòng)化報(bào)告:完整的測(cè)試產(chǎn)物,快速定位問題

最佳實(shí)踐:

  • ? 優(yōu)先使用 Playwright 的隱式等待
  • ? 全局登錄一次,避免重復(fù)
  • ? 使用參數(shù)化進(jìn)行數(shù)據(jù)驅(qū)動(dòng)測(cè)試
  • ? 完整的異常場(chǎng)景覆蓋
  • ? 充分利用 Mock API 提升速度

關(guān)鍵優(yōu)勢(shì):

Playwright 相比 Selenium:

  • 更快的執(zhí)行速度(優(yōu)化程度更高)
  • 更好的等待機(jī)制(自動(dòng)同步)
  • 原生網(wǎng)絡(luò)攔截能力(Mock API)
  • 完整的鏈路追蹤能力(快速定位問題)

參考資源

到此這篇關(guān)于使用Playwright+Pytest構(gòu)建Web UI自動(dòng)化測(cè)試框架完整示例的文章就介紹到這了,更多相關(guān)Playwright+Pytest自動(dòng)化測(cè)試框架內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 使用python實(shí)現(xiàn)PDF本地化壓縮

    使用python實(shí)現(xiàn)PDF本地化壓縮

    這篇文章主要為大家詳細(xì)介紹了如何使用python實(shí)現(xiàn)PDF本地化壓縮功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-08-08
  • Python利用PIL庫實(shí)現(xiàn)圖片轉(zhuǎn)Base64字符串的完整指南

    Python利用PIL庫實(shí)現(xiàn)圖片轉(zhuǎn)Base64字符串的完整指南

    這篇文章主要為大家詳細(xì)介紹了如何使用Python的PIL(Pillow)庫將圖像轉(zhuǎn)換為Base64編碼字符串,并提供多種實(shí)用場(chǎng)景的示例代碼,有需要的小伙伴可以了解下
    2026-01-01
  • 對(duì)python實(shí)現(xiàn)二維函數(shù)高次擬合的示例詳解

    對(duì)python實(shí)現(xiàn)二維函數(shù)高次擬合的示例詳解

    今天小編就為大家分享一篇對(duì)python實(shí)現(xiàn)二維函數(shù)高次擬合的示例詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2018-12-12
  • 通過Python模塊filecmp 對(duì)文件比較的實(shí)現(xiàn)方法

    通過Python模塊filecmp 對(duì)文件比較的實(shí)現(xiàn)方法

    這篇文章主要介紹了通過Python模塊filecmp 對(duì)文件比較的實(shí)現(xiàn)方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2018-06-06
  • python微元法計(jì)算函數(shù)曲線長度的方法

    python微元法計(jì)算函數(shù)曲線長度的方法

    今天小編就為大家分享一篇python微元法計(jì)算函數(shù)曲線長度的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2018-11-11
  • 從入門到精通:玩轉(zhuǎn)Python?Fire庫

    從入門到精通:玩轉(zhuǎn)Python?Fire庫

    想快速打造屬于你的Python?GUI應(yīng)用嗎?拋開復(fù)雜的代碼,用Python?Fire庫就能輕松實(shí)現(xiàn)!本指南將引領(lǐng)你從零起步,駕馭Python?Fire的強(qiáng)大功能,讓編程既簡單又高效,準(zhǔn)備好了嗎?讓我們開始玩轉(zhuǎn)Python?Fire,開啟你的編程冒險(xiǎn)吧!
    2024-02-02
  • OpenCV模板匹配matchTemplate的實(shí)現(xiàn)

    OpenCV模板匹配matchTemplate的實(shí)現(xiàn)

    這篇文章主要介紹了OpenCV模板匹配matchTemplate的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-10-10
  • 利用Python產(chǎn)生加密表和解密表的實(shí)現(xiàn)方法

    利用Python產(chǎn)生加密表和解密表的實(shí)現(xiàn)方法

    這篇文章主要介紹了利用Python產(chǎn)生加密表和解密表的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-10-10
  • Python創(chuàng)建Excel的4種方式小結(jié)

    Python創(chuàng)建Excel的4種方式小結(jié)

    這篇文章主要為大家詳細(xì)介紹了Python中創(chuàng)建Excel的4種常見方式,文中的示例代碼簡潔易懂,具有一定的參考價(jià)值,感興趣的小伙伴可以學(xué)習(xí)一下
    2025-02-02
  • Python實(shí)現(xiàn)原神抽卡的方法

    Python實(shí)現(xiàn)原神抽卡的方法

    這篇文章主要為大家介紹了Python實(shí)現(xiàn)原神抽卡的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2021-12-12

最新評(píng)論

榆社县| 英德市| 宝丰县| 永泰县| 荔波县| 邢台县| 育儿| 阿克苏市| 通化市| 黑龙江省| 三门县| 无为县| 诏安县| 青冈县| 张家口市| 赞皇县| 河东区| 台前县| 涞源县| 杭锦旗| 平塘县| 舟山市| 曲松县| 凉城县| 四会市| 扎赉特旗| 上犹县| 澜沧| 民乐县| 大姚县| 扬州市| 保靖县| 丽江市| 七台河市| 宣威市| 五家渠市| 封开县| 佛冈县| 桑日县| 华坪县| 安顺市|